mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-09 16:00:52 +00:00
Release v0.18.0 - Compact mobile UI, tool expansion controls, smoother streaming, session fixes and Winget automation (#568)
Some checks failed
Release Binaries / release (push) Has been cancelled
Some checks failed
Release Binaries / release (push) Has been cancelled
## Thanks for contributions - [@shantur](https://github.com/shantur): [#535](https://github.com/NeuralNomadsAI/CodeNomad/pull/535), [#556](https://github.com/NeuralNomadsAI/CodeNomad/pull/556), [#557](https://github.com/NeuralNomadsAI/CodeNomad/pull/557), [#562](https://github.com/NeuralNomadsAI/CodeNomad/pull/562), [#563](https://github.com/NeuralNomadsAI/CodeNomad/pull/563), [#564](https://github.com/NeuralNomadsAI/CodeNomad/pull/564), [#565](https://github.com/NeuralNomadsAI/CodeNomad/pull/565), [#567](https://github.com/NeuralNomadsAI/CodeNomad/pull/567) - [@pascalandr](https://github.com/pascalandr): [#533](https://github.com/NeuralNomadsAI/CodeNomad/pull/533), [#551](https://github.com/NeuralNomadsAI/CodeNomad/pull/551) - [@JDis03](https://github.com/JDis03): [#484](https://github.com/NeuralNomadsAI/CodeNomad/pull/484), [#536](https://github.com/NeuralNomadsAI/CodeNomad/pull/536) - [@aayurt](https://github.com/aayurt): [#552](https://github.com/NeuralNomadsAI/CodeNomad/pull/552) ## Highlights - **Compact mode is much better on mobile**: The message UI has been redesigned for compact layouts, with cleaner controls and a denser conversation view that works better on smaller screens. - **Tool output is easier to control**: New tool-level expansion settings let you choose how much tool-call detail you want to see, making busy sessions easier to scan. - **Streaming output feels steadier**: Bash output, tool output, and assistant responses now stay pinned more reliably while streaming, reducing jumps and missed updates. - **Project sessions are scoped more accurately**: Session lists now respect the active project more consistently, so switching projects is less likely to show unrelated sessions. - **Tab closing is more dependable**: Closing tabs has been tightened up to avoid cases where the wrong tab behavior could interrupt your workflow. ## What’s Improved - **Cleaner compact conversation layout**: Message blocks, message sections, tool calls, permissions, questions, todos, and task output have all been polished for a more readable compact experience. - **More useful tool-call presentation**: Tool calls now have a stronger registry-backed rendering path, clearer titles, improved status display, and better per-tool output handling. - **Better follow behavior near the bottom of sessions**: The session view now tracks bottom-follow intent more carefully, helping the app stay with new output when you expect it to. - **Smoother live session creation**: Session-created events from the live event stream are now handled directly, so new sessions appear more reliably without manual refreshes. - **Improved permission denial feedback**: Denied permissions now produce simpler, less noisy feedback in the conversation. ## Fixes - **More reliable autoscroll during long responses**: Hold-mode assistant autoscroll, message-part tool output autoscroll, and streaming bash output pinning were stabilized. - **Less duplicated streaming text**: SSE part deltas are throttled and buffered more carefully to prevent stale delta text from being duplicated in streamed messages. - **Better session list isolation**: Project session list requests are now scoped so sessions from one project do not bleed into another project’s list. - **More stable tab management**: A tab close bug was fixed to make multi-session navigation safer. - **Release pipeline reliability**: Winget release automation is now wired into the release workflow so Windows package updates can be published more consistently. ## Docs - **New contributor guide**: A `CONTRIBUTING.md` guide was added to help new contributors set up the project and understand the development workflow. - **Winget release docs updated**: Supporting documentation for Winget release automation was refreshed. ### Contributors - [@shantur](https://github.com/shantur) - [@pascalandr](https://github.com/pascalandr) - [@JDis03](https://github.com/JDis03) - [@aayurt](https://github.com/aayurt) **Full Changelog**: [v0.17.0...v0.18.0](https://github.com/NeuralNomadsAI/CodeNomad/compare/v0.17.0...v0.18.0) ## Merged Pull Requests ### [@shantur](https://github.com/shantur) - PR [#535](https://github.com/NeuralNomadsAI/CodeNomad/pull/535) “fix(ui): simplify permission denial feedback” - PR [#556](https://github.com/NeuralNomadsAI/CodeNomad/pull/556) “fix(ui): keep streaming bash output pinned” - PR [#557](https://github.com/NeuralNomadsAI/CodeNomad/pull/557) “WIP: Compact mode message UI redesign” - PR [#562](https://github.com/NeuralNomadsAI/CodeNomad/pull/562) “fix(ui): handle session created SSE events” - PR [#563](https://github.com/NeuralNomadsAI/CodeNomad/pull/563) “feat(ui): Add tool level expansion settings.” - PR [#564](https://github.com/NeuralNomadsAI/CodeNomad/pull/564) “Improve compact mobile UI controls” - PR [#565](https://github.com/NeuralNomadsAI/CodeNomad/pull/565) “fix(ui): scope project session list requests” - PR [#567](https://github.com/NeuralNomadsAI/CodeNomad/pull/567) “fix(ui): keep message-part tool output auto-scroll synced” ### [@pascalandr](https://github.com/pascalandr) - PR [#533](https://github.com/NeuralNomadsAI/CodeNomad/pull/533) “fix: stabilize hold-mode assistant autoscroll” - PR [#551](https://github.com/NeuralNomadsAI/CodeNomad/pull/551) “fix: wire Winget automation into release pipeline” ### [@JDis03](https://github.com/JDis03) - PR [#484](https://github.com/NeuralNomadsAI/CodeNomad/pull/484) “docs: add CONTRIBUTING.md guide for new contributors” - PR [#536](https://github.com/NeuralNomadsAI/CodeNomad/pull/536) “fix(ui): throttle SSE part deltas and prevent stale delta text duplication” ### [@aayurt](https://github.com/aayurt) - PR [#552](https://github.com/NeuralNomadsAI/CodeNomad/pull/552) “Bugfix/tab close bug”
This commit is contained in:
commit
6fe942fca9
142 changed files with 5696 additions and 3002 deletions
12
.github/workflows/reusable-release.yml
vendored
12
.github/workflows/reusable-release.yml
vendored
|
|
@ -97,6 +97,18 @@ jobs:
|
|||
release_name: ${{ needs.prepare-release.outputs.release_name }}
|
||||
secrets: inherit
|
||||
|
||||
update-winget:
|
||||
needs:
|
||||
- prepare-release
|
||||
- build-and-upload
|
||||
if: ${{ !inputs.prerelease }}
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/update-winget.yml
|
||||
with:
|
||||
release_tag: ${{ needs.prepare-release.outputs.tag }}
|
||||
secrets: inherit
|
||||
|
||||
release-ui:
|
||||
needs: prepare-release
|
||||
if: ${{ inputs.release_ui }}
|
||||
|
|
|
|||
80
.github/workflows/update-winget.yml
vendored
80
.github/workflows/update-winget.yml
vendored
|
|
@ -1,17 +1,83 @@
|
|||
name: Update Winget
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_call:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: "Stable release tag to inspect"
|
||||
required: true
|
||||
type: string
|
||||
release_id:
|
||||
description: "Optional numeric GitHub release id"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: "Stable release tag to inspect"
|
||||
required: true
|
||||
type: string
|
||||
release_id:
|
||||
description: "Optional numeric GitHub release id"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
resolve-release:
|
||||
name: Resolve release metadata
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_id: ${{ steps.release.outputs.release_id }}
|
||||
release_tag: ${{ steps.release.outputs.release_tag }}
|
||||
draft: ${{ steps.release.outputs.draft }}
|
||||
prerelease: ${{ steps.release.outputs.prerelease }}
|
||||
steps:
|
||||
- name: Resolve release metadata
|
||||
id: release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
RELEASE_ID_INPUT: ${{ inputs.release_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "$RELEASE_ID_INPUT" ]; then
|
||||
release_api="repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID_INPUT}"
|
||||
else
|
||||
release_api="repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}"
|
||||
fi
|
||||
|
||||
release_id="$(gh api "$release_api" --jq '.id')"
|
||||
release_tag="$(gh api "$release_api" --jq '.tag_name')"
|
||||
draft="$(gh api "$release_api" --jq '.draft')"
|
||||
prerelease="$(gh api "$release_api" --jq '.prerelease')"
|
||||
|
||||
if [ -z "$release_id" ] || [ "$release_id" = "null" ]; then
|
||||
echo "Unable to resolve release id for tag '$RELEASE_TAG'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
|
||||
echo "release_tag=$release_tag" >> "$GITHUB_OUTPUT"
|
||||
echo "draft=$draft" >> "$GITHUB_OUTPUT"
|
||||
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log resolved release metadata
|
||||
run: |
|
||||
echo "Release tag: ${{ steps.release.outputs.release_tag }}"
|
||||
echo "Release id: ${{ steps.release.outputs.release_id }}"
|
||||
echo "Draft: ${{ steps.release.outputs.draft }}"
|
||||
echo "Prerelease: ${{ steps.release.outputs.prerelease }}"
|
||||
|
||||
update-winget:
|
||||
name: Submit Winget manifest update
|
||||
if: ${{ !github.event.release.draft && !github.event.release.prerelease }}
|
||||
needs: resolve-release
|
||||
if: ${{ needs.resolve-release.outputs.draft != 'true' && needs.resolve-release.outputs.prerelease != 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
WINGET_PACKAGE_IDENTIFIER: ${{ vars.WINGET_PACKAGE_IDENTIFIER || 'NeuralNomadsAI.CodeNomad' }}
|
||||
|
|
@ -35,8 +101,8 @@ jobs:
|
|||
run: |
|
||||
args=(
|
||||
--repo "${{ github.repository }}"
|
||||
--release-id "${{ github.event.release.id }}"
|
||||
--tag "${{ github.event.release.tag_name }}"
|
||||
--release-id "${{ needs.resolve-release.outputs.release_id }}"
|
||||
--tag "${{ needs.resolve-release.outputs.release_tag }}"
|
||||
--asset-name-template "$WINGET_WINDOWS_ASSET_NAME_TEMPLATE"
|
||||
--timeout-seconds "$WINGET_ASSET_WAIT_TIMEOUT_SECONDS"
|
||||
--poll-interval-seconds "$WINGET_ASSET_POLL_INTERVAL_SECONDS"
|
||||
|
|
@ -79,7 +145,7 @@ jobs:
|
|||
with:
|
||||
identifier: ${{ env.WINGET_PACKAGE_IDENTIFIER }}
|
||||
version: ${{ steps.release_asset.outputs.version }}
|
||||
release-tag: ${{ github.event.release.tag_name }}
|
||||
release-tag: ${{ needs.resolve-release.outputs.release_tag }}
|
||||
installers-regex: ${{ steps.release_asset.outputs.asset_regex }}
|
||||
fork-user: ${{ env.WINGET_FORK_OWNER }}
|
||||
token: ${{ secrets.WINGET_GITHUB_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
- When adding new component styles, place them beside their peers in the scoped subdirectory (e.g., `src/styles/messaging/new-part.css`) and import them from the corresponding aggregator file.
|
||||
- Prefer smaller, focused style files (≈150 lines or less) over large monoliths. Split by component or feature area if a file grows beyond that size.
|
||||
- Co-locate reusable UI patterns (buttons, selectors, dropdowns, etc.) under `src/styles/components/` and avoid redefining the same utility classes elsewhere.
|
||||
- Never use rounded corners in UI styling; keep corners square unless the user explicitly requests otherwise for a specific change.
|
||||
- Document any new styling conventions or directory additions in this file so future changes remain consistent.
|
||||
|
||||
## Coding Principles
|
||||
|
|
|
|||
154
CONTRIBUTING.md
Normal file
154
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# Contributing to CodeNomad
|
||||
|
||||
Thank you for your interest in contributing! This guide will help you get started.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js 18+** and npm
|
||||
- **OpenCode CLI** in your `PATH` (the server connects to the OpenCode binary to manage workspaces)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/NeuralNomadsAI/CodeNomad.git
|
||||
cd CodeNomad
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Finding Issues to Work On
|
||||
|
||||
Browse [open issues](https://github.com/NeuralNomadsAI/CodeNomad/issues) and look for these labels:
|
||||
|
||||
| Label | Meaning |
|
||||
|---|---|
|
||||
| `ready-to-work` | Clear scope, ready for anyone to pick up |
|
||||
| `good-first-issue` | Good for first-time contributors |
|
||||
| `enhancement` | New feature requests |
|
||||
| `bug` | Bug reports |
|
||||
|
||||
**Before starting:** comment on the issue so we can discuss approach and avoid duplicate work.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Fork and Branch
|
||||
|
||||
```bash
|
||||
# Fork the repo on GitHub, then clone your fork
|
||||
git clone https://github.com/YOUR_USERNAME/CodeNomad.git
|
||||
cd CodeNomad
|
||||
|
||||
# Add the upstream remote
|
||||
git remote add upstream https://github.com/NeuralNomadsAI/CodeNomad.git
|
||||
|
||||
# Create a branch from upstream/dev
|
||||
git fetch upstream
|
||||
git checkout -b fix/your-branch-name upstream/dev
|
||||
```
|
||||
|
||||
### 2. Branch Naming
|
||||
|
||||
| Prefix | Use for |
|
||||
|---|---|
|
||||
| `fix/` | Bug fixes |
|
||||
| `feat/` | New features |
|
||||
| `docs/` | Documentation changes |
|
||||
| `refactor/` | Code refactoring |
|
||||
| `chore/` | Build, config, maintenance |
|
||||
|
||||
Examples: `fix/question-queue-ordering`, `feat/retry-tool-call`, `docs/contributing-guide`
|
||||
|
||||
### 3. Make Your Changes
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run the dev server
|
||||
npm run dev
|
||||
|
||||
# Run type checking
|
||||
npm run typecheck --workspace @codenomad/ui
|
||||
```
|
||||
|
||||
### 4. Commit
|
||||
|
||||
Write clear, descriptive commit messages. Explain **what** changed and **why**.
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "fix(ui): preserve question queue order when upserting duplicate requests
|
||||
|
||||
When a question arrives as a global entry and later resolves to a tool
|
||||
part with a newer timestamp, the original enqueue time was lost, causing
|
||||
the question to move behind newer entries and break interruption order."
|
||||
```
|
||||
|
||||
### 5. Push and Create a PR
|
||||
|
||||
```bash
|
||||
git push origin your-branch-name
|
||||
```
|
||||
|
||||
Then open a pull request on GitHub targeting the `dev` branch.
|
||||
|
||||
**PR checklist:**
|
||||
- [ ] Branch is based on latest `upstream/dev`
|
||||
- [ ] One issue per PR (don't mix unrelated changes)
|
||||
- [ ] Type checking passes: `npm run typecheck` (root) or the workspace-specific script matching your change area
|
||||
- [ ] Tests pass (if applicable)
|
||||
- [ ] PR description explains the change, includes relevant screenshots for UI changes, and links related issues when applicable
|
||||
|
||||
## Project Structure
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| `packages/server` | Core logic & CLI — workspaces, OpenCode proxy, API, auth |
|
||||
| `packages/ui` | SolidJS frontend — reactive UI components and stores |
|
||||
| `packages/electron-app` | Electron desktop shell |
|
||||
| `packages/tauri-app` | Tauri desktop shell (experimental) |
|
||||
| `packages/opencode-plugin` | OpenCode plugin integration |
|
||||
| `packages/cloudflare` | Cloudflare deployment adapters |
|
||||
|
||||
### Key UI Files
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `packages/ui/src/stores/session-events.ts` | SSE event handlers (idle, status, permissions, questions) |
|
||||
| `packages/ui/src/stores/session-actions.ts` | User actions (send message, abort, revert, fork) |
|
||||
| `packages/ui/src/stores/message-v2/` | Message store (v2 architecture) |
|
||||
| `packages/ui/src/stores/instances.ts` | Instance management and interruption queues |
|
||||
| `packages/ui/src/components/tool-call.tsx` | Tool call rendering |
|
||||
| `packages/ui/src/components/message-block.tsx` | Message display blocks |
|
||||
| `packages/ui/src/components/session/session-view.tsx` | Main session view |
|
||||
| `packages/ui/src/lib/i18n/messages/` | Translation files (en, es, fr, ja, ru, he, zh-Hans) |
|
||||
|
||||
> For a comprehensive map of all six functional areas (server, UI, desktop, speech/audio, build, Cloudflare), SDK integration patterns, and feature traces, load the `codenomad-architecture-guide` skill:
|
||||
> `.opencode/skills/codenomad-architecture-guide/SKILL.md`
|
||||
|
||||
### Styling
|
||||
|
||||
- Tokens: `packages/ui/src/styles/tokens.css`
|
||||
- Utilities: `packages/ui/src/styles/utilities.css`
|
||||
- Component styles: `packages/ui/src/styles/components/`, `packages/ui/src/styles/messaging/`, `packages/ui/src/styles/panels/`
|
||||
- Keep style files under ~150 lines; split by component
|
||||
|
||||
### Internationalization (i18n)
|
||||
|
||||
- Use `useI18n()` in components, `tGlobal()` in stores
|
||||
- Messages live in `packages/ui/src/lib/i18n/messages/<locale>/`
|
||||
- When adding a string: add to `en/` first, then add the same key to every other locale
|
||||
- Placeholders use `{name}` syntax (word characters only)
|
||||
|
||||
## Code Principles
|
||||
|
||||
- **KISS**: Keep modules narrowly scoped
|
||||
- **DRY**: Share helpers before copy-pasting
|
||||
- **Single responsibility**: Split files when concerns diverge
|
||||
- **Composable primitives**: Prefer signals, hooks, utilities over deep inheritance
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Check existing [issues](https://github.com/NeuralNomadsAI/CodeNomad/issues) and [PRs](https://github.com/NeuralNomadsAI/CodeNomad/pulls)
|
||||
- Ask in the issue you're working on
|
||||
- Review the [server documentation](packages/server/README.md) for CLI flags and configuration
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
# Winget release automation
|
||||
|
||||
CodeNomad publishes Winget updates from GitHub Releases with `.github/workflows/update-winget.yml`.
|
||||
CodeNomad publishes Winget updates from the stable GitHub release pipeline. `.github/workflows/reusable-release.yml` now calls `.github/workflows/update-winget.yml` after the release assets finish uploading.
|
||||
|
||||
## Trigger
|
||||
|
||||
- Runs on `release.published`.
|
||||
- Exits early for draft or prerelease releases.
|
||||
- Runs as a reusable workflow from the stable release pipeline, after `build-and-upload` completes.
|
||||
- Resolves the target release by tag through the GitHub API, then exits early for draft or prerelease releases.
|
||||
- Can also be rerun manually with `workflow_dispatch` by supplying the stable release tag (and optionally the numeric release id).
|
||||
- This avoids the old `release.published` trap where a release created by GitHub Actions with the default `GITHUB_TOKEN` does not fan out into a second workflow run.
|
||||
- Waits for the expected stable Windows Tauri asset because the release record can exist before all assets finish uploading.
|
||||
|
||||
## Required configuration
|
||||
|
|
@ -28,7 +30,7 @@ CodeNomad publishes Winget updates from GitHub Releases with `.github/workflows/
|
|||
|
||||
## Runtime flow
|
||||
|
||||
1. Resolve the release version from `github.event.release.tag_name`.
|
||||
1. Resolve the target release by tag through the GitHub API, then derive the package version from the resolved release tag.
|
||||
2. Poll the release API until exactly one uploaded asset matches the configured Windows Tauri asset template.
|
||||
3. Download the matched asset once and compute a SHA-256 for logging and verification.
|
||||
4. Verify the PAT owner matches `WINGET_FORK_OWNER` and that `${WINGET_FORK_OWNER}/winget-pkgs` is a fork of `microsoft/winget-pkgs`.
|
||||
|
|
@ -38,3 +40,4 @@ CodeNomad publishes Winget updates from GitHub Releases with `.github/workflows/
|
|||
|
||||
- The workflow does not depend on a persistent local `winget-pkgs` clone.
|
||||
- The current package in `winget-pkgs` uses the release ZIP with a nested NSIS installer, so the workflow targets the stable `CodeNomad-Tauri-windows-x64-{version}.zip` asset.
|
||||
- If a maintainer publishes a release outside the standard release workflow, they should manually run `Update Winget` for that stable tag.
|
||||
|
|
|
|||
22
package-lock.json
generated
22
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "codenomad-workspace",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codenomad-workspace",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"7zip-bin": "^5.2.0",
|
||||
|
|
@ -3266,9 +3266,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/sdk": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.16.0.tgz",
|
||||
"integrity": "sha512-S4H2e9j4rdHs5BQOCjmVEdqdXmKwPFKjXPbPUaWiRJpAjBcZ/uIBpoZkmV+x9BLzc+vrE6WAffMZieQgukt4DA==",
|
||||
"version": "1.17.8",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.17.8.tgz",
|
||||
"integrity": "sha512-6MKmsj2ujZyL44jy+12dpwWYDYKPS9fUr+0wVQxaIlPYQ/eAt8T8T3QrybplJ5ZtHfZUX+esXZ02x2UYYm7oEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "7.0.6"
|
||||
|
|
@ -13409,7 +13409,7 @@
|
|||
},
|
||||
"packages/electron-app": {
|
||||
"name": "@neuralnomads/codenomad-electron-app",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"yaml": "^2.4.2"
|
||||
|
|
@ -13436,7 +13436,7 @@
|
|||
},
|
||||
"packages/opencode-plugin": {
|
||||
"name": "@codenomad/codenomad-opencode-plugin",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.3.7"
|
||||
|
|
@ -13448,7 +13448,7 @@
|
|||
},
|
||||
"packages/server": {
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.5.0",
|
||||
|
|
@ -13490,7 +13490,7 @@
|
|||
},
|
||||
"packages/tauri-app": {
|
||||
"name": "@codenomad/tauri-app",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.9.4"
|
||||
|
|
@ -13498,12 +13498,12 @@
|
|||
},
|
||||
"packages/ui": {
|
||||
"name": "@codenomad/ui",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@git-diff-view/solid": "^0.0.8",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@opencode-ai/sdk": "1.16.0",
|
||||
"@opencode-ai/sdk": "^1.17.8",
|
||||
"@solidjs/router": "^0.13.0",
|
||||
"@suid/icons-material": "^0.9.0",
|
||||
"@suid/material": "^0.19.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "codenomad-workspace",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"private": true,
|
||||
"description": "CodeNomad monorepo workspace",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@neuralnomads/codenomad-electron-app",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"description": "CodeNomad - AI coding assistant",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@codenomad/codenomad-opencode-plugin",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
|
|
|||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^11.2.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"description": "CodeNomad Server",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
|
|
|
|||
2
packages/tauri-app/Cargo.lock
generated
2
packages/tauri-app/Cargo.lock
generated
|
|
@ -497,7 +497,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "codenomad-tauri"
|
||||
version = "0.17.0"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@codenomad/tauri-app",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "codenomad-tauri"
|
||||
version = "0.17.0"
|
||||
version = "0.18.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CodeNomad",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"identifier": "ai.neuralnomads.codenomad.client",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev:bootstrap",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@codenomad/ui",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"dependencies": {
|
||||
"@git-diff-view/solid": "^0.0.8",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@opencode-ai/sdk": "1.16.0",
|
||||
"@opencode-ai/sdk": "^1.17.8",
|
||||
"@solidjs/router": "^0.13.0",
|
||||
"@suid/icons-material": "^0.9.0",
|
||||
"@suid/material": "^0.19.0",
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ import {
|
|||
clearActiveParentSession,
|
||||
createSession,
|
||||
fetchSessions,
|
||||
getSessionFetchLimit,
|
||||
updateSessionAgent,
|
||||
updateSessionModel,
|
||||
} from "./stores/sessions"
|
||||
|
|
@ -389,7 +388,7 @@ const App: Component = () => {
|
|||
|
||||
if (!confirmed) return
|
||||
|
||||
await stopInstance(instanceId)
|
||||
stopInstance(instanceId)
|
||||
}
|
||||
|
||||
async function handleNewSession(instanceId: string) {
|
||||
|
|
@ -419,7 +418,7 @@ const App: Component = () => {
|
|||
clearActiveParentSession(instanceId)
|
||||
|
||||
try {
|
||||
await fetchSessions(instanceId, { reset: true, limit: getSessionFetchLimit(instanceId) })
|
||||
await fetchSessions(instanceId, { reset: true })
|
||||
} catch (error) {
|
||||
log.error("Failed to refresh sessions after closing", error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export interface ActionOverflowMenuItem {
|
|||
label: string
|
||||
icon?: JSXElement
|
||||
disabled?: boolean
|
||||
checked?: boolean
|
||||
destructive?: boolean
|
||||
onSelect: () => void | Promise<void>
|
||||
onMouseEnter?: () => void
|
||||
|
|
@ -52,6 +53,8 @@ export default function ActionOverflowMenu(props: ActionOverflowMenuProps) {
|
|||
<DropdownMenu.Item
|
||||
class="action-overflow-item"
|
||||
data-destructive={item.destructive ? "true" : undefined}
|
||||
role={typeof item.checked === "boolean" ? "menuitemcheckbox" : undefined}
|
||||
aria-checked={typeof item.checked === "boolean" ? item.checked : undefined}
|
||||
disabled={item.disabled}
|
||||
onPointerEnter={() => {
|
||||
if (item.disabled) return
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ interface ContextMeterProps {
|
|||
usedLabel: string
|
||||
availableLabel: string
|
||||
class?: string
|
||||
centerValue?: boolean
|
||||
}
|
||||
|
||||
const LABEL_CLASS = "uppercase text-[10px] tracking-wide text-muted"
|
||||
|
|
@ -104,18 +105,32 @@ export const ContextMeter: Component<ContextMeterProps> = (props) => {
|
|||
|
||||
const tooltipText = () => `Context Used: ${percentLabel()}`
|
||||
|
||||
const valuePill = () => (
|
||||
<div class={containerClass}>
|
||||
<span class={LABEL_CLASS}>{props.usedLabel}</span>
|
||||
<span class="font-semibold text-primary tabular-nums">{props.formatTokens(used())}</span>
|
||||
<span class="text-muted">/</span>
|
||||
<span class={LABEL_CLASS}>{props.availableLabel}</span>
|
||||
<span class="font-semibold text-primary tabular-nums">
|
||||
{available() !== null ? props.formatTokens(available() as number) : "--"}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (props.centerValue) {
|
||||
return (
|
||||
<div class="grid grid-cols-[22px_auto_22px] items-center gap-2" title={tooltipText()}>
|
||||
<div class="flex justify-end">{circle()}</div>
|
||||
{valuePill()}
|
||||
<span aria-hidden="true" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="inline-flex items-center gap-2" title={tooltipText()}>
|
||||
{circle()}
|
||||
<div class={containerClass}>
|
||||
<span class={LABEL_CLASS}>{props.usedLabel}</span>
|
||||
<span class="font-semibold text-primary tabular-nums">{props.formatTokens(used())}</span>
|
||||
<span class="text-muted">/</span>
|
||||
<span class={LABEL_CLASS}>{props.availableLabel}</span>
|
||||
<span class="font-semibold text-primary tabular-nums">
|
||||
{available() !== null ? props.formatTokens(available() as number) : "--"}
|
||||
</span>
|
||||
</div>
|
||||
{valuePill()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, For, Show, createMemo, createSignal } from "solid-js"
|
||||
import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import {
|
||||
DragDropProvider,
|
||||
|
|
@ -37,15 +37,9 @@ interface SortableAppTabProps {
|
|||
onClose: (tabId: string) => void
|
||||
}
|
||||
|
||||
const SortableAppTab: Component<SortableAppTabProps> = (props) => {
|
||||
const sortable = createSortable(props.tab.id)
|
||||
|
||||
const AppTabContent: Component<SortableAppTabProps> = (props) => {
|
||||
return (
|
||||
<div
|
||||
ref={sortable}
|
||||
class={`tab-draggable ${sortable.isActiveDraggable ? "tab-draggable-active" : ""}`}
|
||||
data-app-tab-id={props.tab.id}
|
||||
>
|
||||
<>
|
||||
{props.tab.kind === "instance" ? (
|
||||
<InstanceTab
|
||||
instance={props.tab.instance}
|
||||
|
|
@ -82,14 +76,59 @@ const SortableAppTab: Component<SortableAppTabProps> = (props) => {
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SortableAppTab: Component<SortableAppTabProps> = (props) => {
|
||||
const sortable = createSortable(props.tab.id)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={sortable}
|
||||
class={`tab-draggable ${sortable.isActiveDraggable ? "tab-draggable-active" : ""}`}
|
||||
data-app-tab-id={props.tab.id}
|
||||
>
|
||||
<AppTabContent {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StaticAppTab: Component<SortableAppTabProps> = (props) => {
|
||||
return (
|
||||
<div class="tab-draggable" data-app-tab-id={props.tab.id}>
|
||||
<AppTabContent {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const isTouchOnlyPointer = () => {
|
||||
if (typeof window === "undefined") return false
|
||||
return Boolean(window.matchMedia?.("(pointer: coarse)")?.matches && !window.matchMedia?.("(any-pointer: fine)")?.matches)
|
||||
}
|
||||
|
||||
const InstanceTabs: Component<InstanceTabsProps> = (props) => {
|
||||
const { t } = useI18n()
|
||||
const { preferences } = useConfig()
|
||||
const tabIds = createMemo(() => props.tabs.map((tab) => tab.id))
|
||||
const [dragReorderEnabled, setDragReorderEnabled] = createSignal(!isTouchOnlyPointer())
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const coarseQuery = window.matchMedia?.("(pointer: coarse)")
|
||||
const fineQuery = window.matchMedia?.("(any-pointer: fine)")
|
||||
if (!coarseQuery || !fineQuery) return
|
||||
|
||||
const syncDragReorder = () => setDragReorderEnabled(!isTouchOnlyPointer())
|
||||
syncDragReorder()
|
||||
coarseQuery.addEventListener("change", syncDragReorder)
|
||||
fineQuery.addEventListener("change", syncDragReorder)
|
||||
|
||||
onCleanup(() => {
|
||||
coarseQuery.removeEventListener("change", syncDragReorder)
|
||||
fineQuery.removeEventListener("change", syncDragReorder)
|
||||
})
|
||||
})
|
||||
|
||||
/** Whether to show toast history panel */
|
||||
const [showToastHistory, setShowToastHistory] = createSignal(false)
|
||||
|
|
@ -132,22 +171,38 @@ const InstanceTabs: Component<InstanceTabsProps> = (props) => {
|
|||
<div class="tab-scroll">
|
||||
<div class="tab-strip">
|
||||
<div class="tab-strip-tabs">
|
||||
<DragDropProvider collisionDetector={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<DragDropSensors>
|
||||
<SortableProvider ids={tabIds()}>
|
||||
<For each={props.tabs}>
|
||||
{(tab) => (
|
||||
<SortableAppTab
|
||||
tab={tab}
|
||||
activeTabId={props.activeTabId}
|
||||
onSelect={props.onSelect}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
</DragDropSensors>
|
||||
</DragDropProvider>
|
||||
<Show
|
||||
when={dragReorderEnabled()}
|
||||
fallback={
|
||||
<For each={props.tabs}>
|
||||
{(tab) => (
|
||||
<StaticAppTab
|
||||
tab={tab}
|
||||
activeTabId={props.activeTabId}
|
||||
onSelect={props.onSelect}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
}
|
||||
>
|
||||
<DragDropProvider collisionDetector={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<DragDropSensors>
|
||||
<SortableProvider ids={tabIds()}>
|
||||
<For each={props.tabs}>
|
||||
{(tab) => (
|
||||
<SortableAppTab
|
||||
tab={tab}
|
||||
activeTabId={props.activeTabId}
|
||||
onSelect={props.onSelect}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
</DragDropSensors>
|
||||
</DragDropProvider>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="tab-strip-spacer" />
|
||||
<Show when={props.tabs.length > 1}>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import PermissionApprovalModal from "../permission-approval-modal"
|
|||
import SessionView from "../session/session-view"
|
||||
import MessageSection from "../message-section"
|
||||
import PromptAttachmentsBar from "../prompt-input/PromptAttachmentsBar"
|
||||
import ActionOverflowMenu, { type ActionOverflowMenuItem } from "../action-overflow-menu"
|
||||
import { formatTokenTotal } from "../../lib/formatters"
|
||||
import ContextMeter from "../context-meter"
|
||||
import { sseManager } from "../../lib/sse-manager"
|
||||
|
|
@ -156,8 +157,10 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||
})
|
||||
|
||||
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
|
||||
const compactHeaderLayout = createMemo(() => isPhoneLayout() || compactHeaderQuery())
|
||||
const narrowHeaderLayout = createMemo(() => sessionCenterWidthStep() === "narrow")
|
||||
const compactHeaderLayout = createMemo(() => narrowHeaderLayout() || compactHeaderQuery())
|
||||
const mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout())
|
||||
const showCompactFullscreenButton = createMemo(() => isPhoneLayout() && !props.mobileFullscreenMode)
|
||||
const compactPromptLayout = createMemo(() => layoutMode() !== "desktop")
|
||||
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||
const rightPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||
|
|
@ -539,6 +542,24 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||
window.dispatchEvent(new CustomEvent(OPEN_SESSION_SEARCH_EVENT))
|
||||
}
|
||||
|
||||
const narrowHeaderMenuItems = createMemo<ActionOverflowMenuItem[]>(() => {
|
||||
const PreviewIcon = PreviewToggleIcon()
|
||||
return [
|
||||
{
|
||||
key: "search",
|
||||
label: t("instanceShell.chatSearch.openAriaLabel"),
|
||||
icon: <Search class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: handleChatSearchClick,
|
||||
},
|
||||
{
|
||||
key: "preview",
|
||||
label: previewToggleLabel(),
|
||||
icon: <PreviewIcon class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: handlePreviewButtonClick,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const openBackgroundOutput = (process: BackgroundProcess) => {
|
||||
setSelectedBackgroundProcess(process)
|
||||
setShowBackgroundOutput(true)
|
||||
|
|
@ -990,15 +1011,14 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||
when={!compactHeaderLayout()}
|
||||
fallback={
|
||||
<div class="flex flex-col w-full gap-1.5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
|
||||
{renderHeaderLeftSlot()}
|
||||
|
||||
<div class="flex-1 flex items-center justify-center min-w-0">
|
||||
<div class="grid grid-cols-[1fr_auto_1fr] items-center gap-2 w-full">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
{renderHeaderLeftSlot()}
|
||||
{renderSessionHeaderIndicators()}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-1">
|
||||
<Show when={!showingInfoView()}>
|
||||
<Show when={!showingInfoView() && !narrowHeaderLayout()}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={handleChatSearchClick}
|
||||
|
|
@ -1023,55 +1043,85 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex items-center justify-center min-w-0">
|
||||
<div class="flex flex-1 items-center justify-end gap-1 min-w-0">
|
||||
<span
|
||||
class={`status-indicator ${connectionStatusClass()}`}
|
||||
aria-label={t("instanceShell.connection.ariaLabel", { status: connectionStatusLabel() })}
|
||||
>
|
||||
<span class="status-dot" />
|
||||
</span>
|
||||
|
||||
<Show when={!isPhoneLayout() && !narrowHeaderLayout()}>
|
||||
{renderPreviewToggleButton()}
|
||||
</Show>
|
||||
|
||||
<Show when={showCompactFullscreenButton() && !narrowHeaderLayout()}>
|
||||
{renderPreviewToggleButton()}
|
||||
</Show>
|
||||
|
||||
<Show when={rightDrawerState() === "floating-closed"}>
|
||||
<IconButton
|
||||
ref={setRightToggleButtonEl}
|
||||
color="inherit"
|
||||
onClick={handleRightAppBarButtonClick}
|
||||
aria-label={rightAppBarButtonLabel()}
|
||||
size="small"
|
||||
aria-expanded={rightDrawerState() !== "floating-closed"}
|
||||
>
|
||||
{rightAppBarButtonIcon()}
|
||||
</IconButton>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={!isPhoneLayout()}>
|
||||
{renderPreviewToggleButton()}
|
||||
</Show>
|
||||
|
||||
<Show when={isPhoneLayout() && !props.mobileFullscreenMode}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={props.onEnterMobileFullscreen}
|
||||
aria-label={t("instanceShell.fullscreen.enter")}
|
||||
title={t("instanceShell.fullscreen.enter")}
|
||||
size="small"
|
||||
>
|
||||
<Maximize2 class="w-5 h-5" aria-hidden="true" />
|
||||
</IconButton>
|
||||
{renderPreviewToggleButton()}
|
||||
</Show>
|
||||
|
||||
<Show when={rightDrawerState() === "floating-closed"}>
|
||||
<IconButton
|
||||
ref={setRightToggleButtonEl}
|
||||
color="inherit"
|
||||
onClick={handleRightAppBarButtonClick}
|
||||
aria-label={rightAppBarButtonLabel()}
|
||||
size="small"
|
||||
aria-expanded={rightDrawerState() !== "floating-closed"}
|
||||
>
|
||||
{rightAppBarButtonIcon()}
|
||||
</IconButton>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
|
||||
<Show when={!showingInfoView()}>
|
||||
<ContextMeter
|
||||
usedTokens={tokenStats().used}
|
||||
availableTokens={tokenStats().avail}
|
||||
formatTokens={formatTokenTotal}
|
||||
usedLabel={t("instanceShell.metrics.usedLabel")}
|
||||
availableLabel={t("instanceShell.metrics.availableLabel")}
|
||||
/>
|
||||
<div
|
||||
class={
|
||||
narrowHeaderLayout() || showCompactFullscreenButton()
|
||||
? "grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2 pb-1"
|
||||
: "flex flex-wrap items-center justify-center gap-2 pb-1"
|
||||
}
|
||||
>
|
||||
<Show when={narrowHeaderLayout() || showCompactFullscreenButton()}>
|
||||
<div class="flex min-w-0 items-center justify-start">
|
||||
<Show when={narrowHeaderLayout() && !showingInfoView()}>
|
||||
<ActionOverflowMenu
|
||||
items={narrowHeaderMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button"
|
||||
minItems={1}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<Show when={!showingInfoView()}>
|
||||
<ContextMeter
|
||||
usedTokens={tokenStats().used}
|
||||
availableTokens={tokenStats().avail}
|
||||
formatTokens={formatTokenTotal}
|
||||
usedLabel={t("instanceShell.metrics.usedLabel")}
|
||||
availableLabel={t("instanceShell.metrics.availableLabel")}
|
||||
centerValue={narrowHeaderLayout() || showCompactFullscreenButton()}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={narrowHeaderLayout() || showCompactFullscreenButton()}>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<Show when={showCompactFullscreenButton()}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={props.onEnterMobileFullscreen}
|
||||
aria-label={t("instanceShell.fullscreen.enter")}
|
||||
title={t("instanceShell.fullscreen.enter")}
|
||||
size="small"
|
||||
sx={{ width: 30, height: 30 }}
|
||||
>
|
||||
<Maximize2 class="w-5 h-5" aria-hidden="true" />
|
||||
</IconButton>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { For, Index, Match, Show, Suspense, Switch, createEffect, createMemo, createSignal, lazy, onCleanup, untrack, type Accessor } from "solid-js"
|
||||
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, ListStart, Trash, Volume2 } from "lucide-solid"
|
||||
import { CheckSquare2, Copy, ExternalLink, FoldVertical, ListStart, Square, Trash, Volume2 } from "lucide-solid"
|
||||
import MessageItem from "./message-item"
|
||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||
import type { ClientPart, MessageInfo } from "../types/message"
|
||||
|
|
@ -15,11 +15,12 @@ import { deleteMessage } from "../stores/session-actions"
|
|||
import { useI18n } from "../lib/i18n"
|
||||
import type { DeleteHoverState } from "../types/delete-hover"
|
||||
import { useSpeech } from "../lib/hooks/use-speech"
|
||||
import SpeechActionButton from "./speech-action-button"
|
||||
import { createFollowScroll } from "../lib/follow-scroll"
|
||||
import { formatElapsedClock, getMessageStartedAt, getPartStartedAt, inferReasoningDurationMs } from "../lib/message-timing"
|
||||
import { inferReasoningDurationMs } from "../lib/message-timing"
|
||||
import type { SessionSearchMatch } from "../lib/session-search"
|
||||
import ActionOverflowMenu, { type ActionOverflowMenuItem } from "./action-overflow-menu"
|
||||
import { copyToClipboard } from "../lib/clipboard"
|
||||
import SpeechActionButton from "./speech-action-button"
|
||||
|
||||
function DeleteUpToIcon() {
|
||||
return (
|
||||
|
|
@ -29,10 +30,9 @@ function DeleteUpToIcon() {
|
|||
)
|
||||
}
|
||||
|
||||
const TOOL_ICON = "🔧"
|
||||
const USER_BORDER_COLOR = "var(--message-user-border)"
|
||||
const ASSISTANT_BORDER_COLOR = "var(--message-assistant-border)"
|
||||
const TOOL_BORDER_COLOR = "var(--message-tool-border)"
|
||||
const NO_STEP_BORDER = "none"
|
||||
const REASONING_SCROLL_SENTINEL_MARGIN_PX = 48
|
||||
|
||||
const LazyToolCall = lazy(() => import("./tool-call"))
|
||||
|
|
@ -391,6 +391,7 @@ function MessageContentItem(props: MessageContentItemProps) {
|
|||
parts={visibleParts()}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
contentStartPartId={props.startPartId}
|
||||
isQueued={isQueued()}
|
||||
showAgentMeta={showAgentMeta()}
|
||||
showDeleteMessage={props.showDeleteMessage}
|
||||
|
|
@ -461,7 +462,6 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||
})
|
||||
|
||||
const toolState = createMemo(() => toolPart()?.state as ToolState | undefined)
|
||||
const toolName = createMemo(() => toolPart()?.tool || "")
|
||||
const messageVersion = createMemo(() => record()?.revision ?? 0)
|
||||
const partVersion = createMemo(() => partEntry()?.revision ?? 0)
|
||||
|
||||
|
|
@ -480,41 +480,12 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||
return findTaskSessionLocation(id, props.instanceId)
|
||||
})
|
||||
|
||||
const handleGoToTaskSession = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const location = taskLocation()
|
||||
if (!location) return
|
||||
navigateToTaskSession(location)
|
||||
}
|
||||
|
||||
const goToTaskSession = () => {
|
||||
const location = taskLocation()
|
||||
if (!location) return
|
||||
navigateToTaskSession(location)
|
||||
}
|
||||
|
||||
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (!props.showDeleteMessage) return
|
||||
if (deletingMessage()) return
|
||||
|
||||
setDeletingMessage(true)
|
||||
try {
|
||||
await deleteMessage(props.instanceId, props.sessionId, props.messageId)
|
||||
} catch (error) {
|
||||
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
variant: "error",
|
||||
})
|
||||
} finally {
|
||||
setDeletingMessage(false)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteUpTo = async () => {
|
||||
if (!props.showDeleteMessage) return
|
||||
if (!props.onDeleteMessagesUpTo) return
|
||||
|
|
@ -528,15 +499,23 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const handleDeleteUpTo = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
await deleteUpTo()
|
||||
}
|
||||
|
||||
const actionMenuItems = (): ActionOverflowMenuItem[] => {
|
||||
const items: ActionOverflowMenuItem[] = []
|
||||
|
||||
if (props.showDeleteMessage) {
|
||||
items.push({
|
||||
key: "select",
|
||||
label: isSelectedForDeletion()
|
||||
? t("messageItem.selection.deselectForDeletion")
|
||||
: t("messageItem.selection.selectForDeletion"),
|
||||
icon: isSelectedForDeletion()
|
||||
? <CheckSquare2 class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
: <Square class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
checked: isSelectedForDeletion(),
|
||||
onSelect: () => props.onToggleSelectedMessage?.(props.messageId, !isSelectedForDeletion()),
|
||||
})
|
||||
}
|
||||
|
||||
if (taskSessionId()) {
|
||||
items.push({
|
||||
key: "go-to-session",
|
||||
|
|
@ -593,81 +572,6 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||
<Show when={toolPart()}>
|
||||
{(resolvedToolPart) => (
|
||||
<div class="delete-hover-scope" data-delete-part-hover={isDeleteOverlayActive() ? "true" : undefined}>
|
||||
<div class="tool-call-header-label" data-action-overflow={actionMenuItems().length > 1 ? "true" : undefined}>
|
||||
<div class="tool-call-header-meta">
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<input
|
||||
class="message-select-checkbox"
|
||||
type="checkbox"
|
||||
checked={isSelectedForDeletion()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||
props.onToggleSelectedMessage?.(props.messageId, next)
|
||||
}}
|
||||
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<span class="tool-call-icon">{TOOL_ICON}</span>
|
||||
<span>{t("messageBlock.tool.header")}</span>
|
||||
<span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span>
|
||||
</div>
|
||||
|
||||
<div class="tool-call-header-actions flex items-center gap-0">
|
||||
<Show when={taskSessionId()}>
|
||||
<button
|
||||
class="tool-call-header-button"
|
||||
type="button"
|
||||
disabled={!taskLocation()}
|
||||
onClick={handleGoToTaskSession}
|
||||
title={t("messageBlock.tool.goToSession.label")}
|
||||
aria-label={t("messageBlock.tool.goToSession.label")}
|
||||
>
|
||||
<ExternalLink class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<button
|
||||
class="tool-call-header-button"
|
||||
type="button"
|
||||
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
|
||||
onClick={handleDeleteUpTo}
|
||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId })}
|
||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||
title={t("messageItem.actions.deleteMessagesUpTo")}
|
||||
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
|
||||
>
|
||||
<DeleteUpToIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tool-call-header-button"
|
||||
type="button"
|
||||
disabled={deletingMessage()}
|
||||
onClick={handleDeleteMessage}
|
||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
>
|
||||
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="tool-call-header-button"
|
||||
minItems={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<ToolCallFallback />}>
|
||||
<LazyToolCall
|
||||
toolCall={resolvedToolPart()}
|
||||
|
|
@ -678,6 +582,7 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
onContentRendered={props.onContentRendered}
|
||||
headerMenuItems={actionMenuItems}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
|
@ -898,7 +803,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||
}
|
||||
items.push(toolItem)
|
||||
blockToolKeys.push(key)
|
||||
lastAccentColor = TOOL_BORDER_COLOR
|
||||
lastAccentColor = NO_STEP_BORDER
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1171,9 +1076,7 @@ function CompactionCard(props: CompactionCardProps) {
|
|||
|
||||
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
||||
|
||||
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const deleteMessageNow = async () => {
|
||||
if (!props.showDeleteMessage) return
|
||||
if (!canDeleteMessage()) return
|
||||
setDeletingMessage(true)
|
||||
|
|
@ -1190,9 +1093,7 @@ function CompactionCard(props: CompactionCardProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const handleDeleteUpTo = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const deleteUpTo = async () => {
|
||||
if (!props.showDeleteMessage) return
|
||||
if (!props.onDeleteMessagesUpTo) return
|
||||
if (deletingUpTo()) return
|
||||
|
|
@ -1205,6 +1106,43 @@ function CompactionCard(props: CompactionCardProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const actionMenuItems = (): ActionOverflowMenuItem[] => {
|
||||
if (!props.showDeleteMessage) return []
|
||||
return [
|
||||
{
|
||||
key: "select",
|
||||
label: isSelectedForDeletion()
|
||||
? t("messageItem.selection.deselectForDeletion")
|
||||
: t("messageItem.selection.selectForDeletion"),
|
||||
icon: isSelectedForDeletion()
|
||||
? <CheckSquare2 class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
: <Square class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
checked: isSelectedForDeletion(),
|
||||
onSelect: () => props.onToggleSelectedMessage?.(props.messageId, !isSelectedForDeletion()),
|
||||
},
|
||||
{
|
||||
key: "delete-up-to",
|
||||
label: t("messageItem.actions.deleteMessagesUpTo"),
|
||||
icon: <DeleteUpToIcon />,
|
||||
disabled: !props.onDeleteMessagesUpTo || deletingUpTo(),
|
||||
destructive: true,
|
||||
onMouseEnter: () => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId }),
|
||||
onMouseLeave: () => props.onDeleteHoverChange?.({ kind: "none" }),
|
||||
onSelect: deleteUpTo,
|
||||
},
|
||||
{
|
||||
key: "delete-message",
|
||||
label: deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage"),
|
||||
icon: <Trash class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
disabled: !canDeleteMessage(),
|
||||
destructive: true,
|
||||
onMouseEnter: () => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId }),
|
||||
onMouseLeave: () => props.onDeleteHoverChange?.({ kind: "none" }),
|
||||
onSelect: deleteMessageNow,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`delete-hover-scope ${containerClass()} relative`}
|
||||
|
|
@ -1212,55 +1150,16 @@ function CompactionCard(props: CompactionCardProps) {
|
|||
role="status"
|
||||
aria-label={t("messageBlock.compaction.ariaLabel")}
|
||||
>
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-header-button"
|
||||
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
|
||||
onClick={handleDeleteUpTo}
|
||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId })}
|
||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||
title={t("messageItem.actions.deleteMessagesUpTo")}
|
||||
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
|
||||
>
|
||||
<DeleteUpToIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-header-button"
|
||||
disabled={!canDeleteMessage()}
|
||||
onClick={handleDeleteMessage}
|
||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
>
|
||||
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button"
|
||||
minItems={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="message-compaction-row">
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<input
|
||||
class="message-select-checkbox"
|
||||
type="checkbox"
|
||||
checked={isSelectedForDeletion()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||
props.onToggleSelectedMessage?.(props.messageId, next)
|
||||
}}
|
||||
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<FoldVertical class="message-compaction-icon w-4 h-4" aria-hidden="true" />
|
||||
<span class="message-compaction-label">{label()}</span>
|
||||
</div>
|
||||
|
|
@ -1321,7 +1220,14 @@ function StepCard(props: StepCardProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const finishStyle = () => (props.borderColor ? { "border-left-color": props.borderColor } : undefined)
|
||||
const finishStyle = () => {
|
||||
if (props.borderColor === NO_STEP_BORDER) {
|
||||
return {
|
||||
"border-inline-start": "none",
|
||||
}
|
||||
}
|
||||
return props.borderColor ? { "border-left-color": props.borderColor } : undefined
|
||||
}
|
||||
let didReportUsageStats = false
|
||||
|
||||
createEffect(() => {
|
||||
|
|
@ -1334,9 +1240,7 @@ function StepCard(props: StepCardProps) {
|
|||
const canDeleteMessage = () =>
|
||||
Boolean(props.showDeleteMessage && props.instanceId && props.sessionId && props.messageId) && !deletingMessage()
|
||||
|
||||
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const handleDeleteMessage = async () => {
|
||||
if (!canDeleteMessage()) return
|
||||
setDeletingMessage(true)
|
||||
try {
|
||||
|
|
@ -1352,9 +1256,7 @@ function StepCard(props: StepCardProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const handleDeleteUpTo = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const handleDeleteUpTo = async () => {
|
||||
if (!props.messageId) return
|
||||
if (!props.onDeleteMessagesUpTo) return
|
||||
if (deletingUpTo()) return
|
||||
|
|
@ -1367,6 +1269,43 @@ function StepCard(props: StepCardProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const actionMenuItems = (): ActionOverflowMenuItem[] => {
|
||||
if (!props.showDeleteMessage || !props.messageId) return []
|
||||
return [
|
||||
{
|
||||
key: "select",
|
||||
label: isSelectedForDeletion()
|
||||
? t("messageItem.selection.deselectForDeletion")
|
||||
: t("messageItem.selection.selectForDeletion"),
|
||||
icon: isSelectedForDeletion()
|
||||
? <CheckSquare2 class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
: <Square class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
checked: isSelectedForDeletion(),
|
||||
onSelect: () => props.onToggleSelectedMessage?.(props.messageId!, !isSelectedForDeletion()),
|
||||
},
|
||||
{
|
||||
key: "delete-up-to",
|
||||
label: t("messageItem.actions.deleteMessagesUpTo"),
|
||||
icon: <DeleteUpToIcon />,
|
||||
disabled: !props.onDeleteMessagesUpTo || deletingUpTo(),
|
||||
destructive: true,
|
||||
onMouseEnter: () => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId! }),
|
||||
onMouseLeave: () => props.onDeleteHoverChange?.({ kind: "none" }),
|
||||
onSelect: handleDeleteUpTo,
|
||||
},
|
||||
{
|
||||
key: "delete-message",
|
||||
label: deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage"),
|
||||
icon: <Trash class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
disabled: !canDeleteMessage(),
|
||||
destructive: true,
|
||||
onMouseEnter: () => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId! }),
|
||||
onMouseLeave: () => props.onDeleteHoverChange?.({ kind: "none" }),
|
||||
onSelect: handleDeleteMessage,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => {
|
||||
const entries = [
|
||||
|
|
@ -1398,51 +1337,14 @@ function StepCard(props: StepCardProps) {
|
|||
}
|
||||
return (
|
||||
<div class={`message-step-card message-step-finish message-step-finish-flush relative`} style={finishStyle()}>
|
||||
<Show when={props.showDeleteMessage && props.messageId}>
|
||||
<input
|
||||
class="message-select-checkbox absolute left-2 top-1/2 -translate-y-1/2"
|
||||
type="checkbox"
|
||||
checked={isSelectedForDeletion()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||
props.onToggleSelectedMessage?.(props.messageId!, next)
|
||||
}}
|
||||
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="message-action-button"
|
||||
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
|
||||
onClick={handleDeleteUpTo}
|
||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId! })}
|
||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||
title={t("messageItem.actions.deleteMessagesUpTo")}
|
||||
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
|
||||
>
|
||||
<DeleteUpToIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="message-action-button"
|
||||
disabled={!canDeleteMessage()}
|
||||
onClick={handleDeleteMessage}
|
||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId! })}
|
||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
>
|
||||
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button"
|
||||
minItems={1}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
|
@ -1456,24 +1358,6 @@ function StepCard(props: StepCardProps) {
|
|||
<div class="message-step-heading">
|
||||
<div class="message-step-title">
|
||||
<div class="message-step-title-left">
|
||||
<Show when={props.showDeleteMessage && props.messageId}>
|
||||
<input
|
||||
class="message-select-checkbox"
|
||||
type="checkbox"
|
||||
checked={isSelectedForDeletion()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||
props.onToggleSelectedMessage?.(props.messageId!, next)
|
||||
}}
|
||||
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={props.showAgentMeta && (agentIdentifier() || modelIdentifier())}>
|
||||
<span class="message-step-meta-inline">
|
||||
<Show when={agentIdentifier()}>{(value) => <span>{t("messageBlock.step.agentLabel", { agent: value() })}</span>}</Show>
|
||||
|
|
@ -1481,7 +1365,17 @@ function StepCard(props: StepCardProps) {
|
|||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<span class="message-step-time">{timestamp()}</span>
|
||||
<span class="message-step-right">
|
||||
<span class="message-step-time">{timestamp()}</span>
|
||||
<Show when={props.showDeleteMessage && props.messageId}>
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button"
|
||||
minItems={1}
|
||||
/>
|
||||
</Show>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1580,7 +1474,7 @@ function ReasoningStreamOutput(props: {
|
|||
}
|
||||
|
||||
function ReasoningCard(props: ReasoningCardProps) {
|
||||
const { locale, t } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded))
|
||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||
|
|
@ -1597,14 +1491,6 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
}
|
||||
})
|
||||
|
||||
const timestamp = () => {
|
||||
const value = getPartStartedAt(props.part) ?? getMessageStartedAt(props.messageInfo) ?? Date.now()
|
||||
const date = new Date(value)
|
||||
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
||||
}
|
||||
|
||||
const durationLabel = () => formatElapsedClock(props.durationMs, locale())
|
||||
|
||||
const agentIdentifier = () => {
|
||||
const info = props.messageInfo
|
||||
if (!info || info.role !== "assistant") return ""
|
||||
|
|
@ -1620,8 +1506,6 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
return modelID
|
||||
}
|
||||
|
||||
const hasMeta = () => Boolean(props.showAgentMeta && (agentIdentifier() || modelIdentifier()))
|
||||
|
||||
const reasoningText = () => {
|
||||
const part = props.part as any
|
||||
if (!part) return ""
|
||||
|
|
@ -1657,10 +1541,55 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
return ""
|
||||
}
|
||||
|
||||
const toggle = () => setExpanded((prev) => !prev)
|
||||
const extractedTitle = () => {
|
||||
const firstLine = reasoningText()
|
||||
.split(/\r?\n/)
|
||||
.map((line: string) => line.trim())
|
||||
.find((line: string) => line.length > 0)
|
||||
if (!firstLine) return ""
|
||||
|
||||
const viewHideLabel = () =>
|
||||
expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")
|
||||
const match = firstLine.match(/^\*\*([^*]+)\*\*/)
|
||||
return match?.[1]?.trim() ?? ""
|
||||
}
|
||||
|
||||
const thoughtDurationTitle = () => {
|
||||
const duration = props.durationMs
|
||||
if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0) {
|
||||
return t("messageBlock.reasoning.thoughtsFallback")
|
||||
}
|
||||
|
||||
const seconds = Math.max(1, Math.round(duration / 1000))
|
||||
if (seconds < 60) {
|
||||
return seconds === 1
|
||||
? t("messageBlock.reasoning.thoughtFor.seconds.one", { count: String(seconds) })
|
||||
: t("messageBlock.reasoning.thoughtFor.seconds.other", { count: String(seconds) })
|
||||
}
|
||||
|
||||
const minutes = Math.max(1, Math.round(seconds / 60))
|
||||
if (minutes < 60) {
|
||||
return minutes === 1
|
||||
? t("messageBlock.reasoning.thoughtFor.minutes.one", { count: String(minutes) })
|
||||
: t("messageBlock.reasoning.thoughtFor.minutes.other", { count: String(minutes) })
|
||||
}
|
||||
|
||||
const hours = Math.max(1, Math.round(minutes / 60))
|
||||
return hours === 1
|
||||
? t("messageBlock.reasoning.thoughtFor.hours.one", { count: String(hours) })
|
||||
: t("messageBlock.reasoning.thoughtFor.hours.other", { count: String(hours) })
|
||||
}
|
||||
|
||||
const reasoningTitle = () => extractedTitle() || thoughtDurationTitle()
|
||||
|
||||
const reasoningMetaTooltip = () => {
|
||||
const parts: string[] = [thoughtDurationTitle()]
|
||||
const agent = agentIdentifier()
|
||||
const model = modelIdentifier()
|
||||
if (agent) parts.push(t("messageBlock.step.agentLabel", { agent }))
|
||||
if (model) parts.push(t("messageBlock.step.modelLabel", { model }))
|
||||
return parts.join("\n")
|
||||
}
|
||||
|
||||
const toggle = () => setExpanded((prev) => !prev)
|
||||
|
||||
const speech = useSpeech({
|
||||
id: () => `${props.instanceId}:${props.sessionId}:${props.messageId}:${(props.part as any)?.id ?? "reasoning"}`,
|
||||
|
|
@ -1671,6 +1600,12 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
|
||||
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
||||
|
||||
const handleCopyReasoning = async () => {
|
||||
const text = reasoningText()
|
||||
if (!text.trim()) return
|
||||
await copyToClipboard(text)
|
||||
}
|
||||
|
||||
const deleteUpTo = async () => {
|
||||
if (!props.showDeleteMessage) return
|
||||
if (!props.onDeleteMessagesUpTo) return
|
||||
|
|
@ -1684,18 +1619,41 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const actionMenuItems = (): ActionOverflowMenuItem[] => {
|
||||
const actionMenuItems = (includePrimaryActions = false): ActionOverflowMenuItem[] => {
|
||||
const items: ActionOverflowMenuItem[] = []
|
||||
|
||||
if (canSpeakReasoning()) {
|
||||
if (props.showDeleteMessage) {
|
||||
items.push({
|
||||
key: "speak",
|
||||
label: speech.buttonTitle(),
|
||||
icon: <Volume2 class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => void speech.toggle(),
|
||||
key: "select",
|
||||
label: isSelectedForDeletion()
|
||||
? t("messageItem.selection.deselectForDeletion")
|
||||
: t("messageItem.selection.selectForDeletion"),
|
||||
icon: isSelectedForDeletion()
|
||||
? <CheckSquare2 class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
: <Square class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
checked: isSelectedForDeletion(),
|
||||
onSelect: () => props.onToggleSelectedMessage?.(props.messageId, !isSelectedForDeletion()),
|
||||
})
|
||||
}
|
||||
|
||||
if (includePrimaryActions) {
|
||||
items.push({
|
||||
key: "copy",
|
||||
label: t("messageBlock.reasoning.copyTitle"),
|
||||
icon: <Copy class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: handleCopyReasoning,
|
||||
})
|
||||
|
||||
if (canSpeakReasoning()) {
|
||||
items.push({
|
||||
key: "speak",
|
||||
label: speech.buttonTitle(),
|
||||
icon: <Volume2 class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => void speech.toggle(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (props.showDeleteMessage) {
|
||||
items.push(
|
||||
{
|
||||
|
|
@ -1738,31 +1696,6 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
return items
|
||||
}
|
||||
|
||||
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!props.showDeleteMessage) return
|
||||
if (!canDeleteMessage()) return
|
||||
setDeletingMessage(true)
|
||||
try {
|
||||
await deleteMessage(props.instanceId, props.sessionId, props.messageId)
|
||||
} catch (error) {
|
||||
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
variant: "error",
|
||||
})
|
||||
} finally {
|
||||
setDeletingMessage(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteUpTo = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
await deleteUpTo()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class="delete-hover-scope message-reasoning-card"
|
||||
|
|
@ -1776,35 +1709,30 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
aria-expanded={expanded()}
|
||||
aria-label={expanded() ? t("messageBlock.reasoning.collapseAriaLabel") : t("messageBlock.reasoning.expandAriaLabel")}
|
||||
>
|
||||
<span class="message-reasoning-disclosure" aria-hidden="true">{expanded() ? "▼" : "▶"}</span>
|
||||
<span class="message-reasoning-label">
|
||||
<span class="message-reasoning-label-primary">
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<input
|
||||
class="message-select-checkbox"
|
||||
type="checkbox"
|
||||
checked={isSelectedForDeletion()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||
props.onToggleSelectedMessage?.(props.messageId, next)
|
||||
}}
|
||||
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<span>{t("messageBlock.reasoning.thinkingLabel")}</span>
|
||||
<Show when={durationLabel()}>
|
||||
{(value) => <span class="message-reasoning-duration">{value()}</span>}
|
||||
</Show>
|
||||
<span class="message-reasoning-type">{t("messageBlock.reasoning.thinkingLabel")}</span>
|
||||
<span class="message-reasoning-title" title={reasoningMetaTooltip() || undefined}>
|
||||
{reasoningTitle()}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="message-reasoning-actions" data-action-overflow={actionMenuItems().length > 1 ? "true" : undefined}>
|
||||
<div class="message-reasoning-actions" data-action-overflow={actionMenuItems(true).length > 0 ? "true" : undefined}>
|
||||
<button
|
||||
type="button"
|
||||
class="message-action-button"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
void handleCopyReasoning()
|
||||
}}
|
||||
aria-label={t("messageBlock.reasoning.copyAriaLabel")}
|
||||
title={t("messageBlock.reasoning.copyTitle")}
|
||||
>
|
||||
<Copy class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<Show when={canSpeakReasoning()}>
|
||||
<SpeechActionButton
|
||||
class="message-action-button"
|
||||
|
|
@ -1819,80 +1747,21 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||
/>
|
||||
</Show>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="message-action-button message-reasoning-primary-action"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
toggle()
|
||||
}}
|
||||
aria-label={viewHideLabel()}
|
||||
title={viewHideLabel()}
|
||||
>
|
||||
<Show when={expanded()} fallback={<ChevronsUpDown class="w-3.5 h-3.5" aria-hidden="true" />}>
|
||||
<ChevronsDownUp class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</Show>
|
||||
</button>
|
||||
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<button
|
||||
type="button"
|
||||
class="message-action-button"
|
||||
onClick={handleDeleteUpTo}
|
||||
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
|
||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId })}
|
||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
|
||||
title={t("messageItem.actions.deleteMessagesUpTo")}
|
||||
>
|
||||
<DeleteUpToIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="message-action-button"
|
||||
onClick={handleDeleteMessage}
|
||||
disabled={!canDeleteMessage()}
|
||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
>
|
||||
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button"
|
||||
minItems={2}
|
||||
triggerClass="message-action-button action-overflow-wide"
|
||||
minItems={1}
|
||||
/>
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems(true)}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button action-overflow-narrow"
|
||||
minItems={1}
|
||||
/>
|
||||
|
||||
<div class="message-reasoning-timing">
|
||||
<span class="message-reasoning-time">{timestamp()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={hasMeta()}>
|
||||
<div class="message-reasoning-meta-row">
|
||||
<span class="message-step-meta-inline">
|
||||
<Show when={agentIdentifier()}>
|
||||
{(value) => (
|
||||
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.agentLabel", { agent: value() })}</span>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={modelIdentifier()}>
|
||||
{(value) => (
|
||||
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.modelLabel", { model: value() })}</span>
|
||||
)}
|
||||
</Show>
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={expanded()}>
|
||||
<div class="message-reasoning-expanded">
|
||||
<div class="message-reasoning-body">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { For, Show, createEffect, createSignal, onCleanup } from "solid-js"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { Copy, ListStart, Split, Trash, Undo, Volume2 } from "lucide-solid"
|
||||
import { CheckSquare2, Copy, ListStart, Split, Square, Trash, Undo, Volume2 } from "lucide-solid"
|
||||
import type { MessageInfo, ClientPart, SDKAssistantMessageV2 } from "../types/message"
|
||||
import { isHiddenSyntheticTextPart, partHasRenderableText } from "../types/message"
|
||||
import type { MessageRecord } from "../stores/message-v2/types"
|
||||
|
|
@ -12,9 +12,9 @@ import { deleteMessage } from "../stores/session-actions"
|
|||
import { isTauriHost } from "../lib/runtime-env"
|
||||
import type { DeleteHoverState } from "../types/delete-hover"
|
||||
import { useSpeech } from "../lib/hooks/use-speech"
|
||||
import SpeechActionButton from "./speech-action-button"
|
||||
import ActionOverflowMenu, { type ActionOverflowMenuItem } from "./action-overflow-menu"
|
||||
import { formatElapsedClock, getMessageDurationMs, getMessageStartedAt } from "../lib/message-timing"
|
||||
import { getMessageDurationMs, getMessageStartedAt } from "../lib/message-timing"
|
||||
import SpeechActionButton from "./speech-action-button"
|
||||
|
||||
function DeleteUpToIcon() {
|
||||
return (
|
||||
|
|
@ -37,13 +37,14 @@ interface MessageItemProps {
|
|||
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||
onFork?: (messageId?: string) => void
|
||||
showAgentMeta?: boolean
|
||||
contentStartPartId?: string
|
||||
onContentRendered?: () => void
|
||||
showDeleteMessage?: boolean
|
||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||
}
|
||||
|
||||
export default function MessageItem(props: MessageItemProps) {
|
||||
const { locale, t } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||
|
|
@ -107,7 +108,7 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
let metaMeasureEl: HTMLSpanElement | undefined
|
||||
const [showMetaInline, setShowMetaInline] = createSignal(true)
|
||||
|
||||
const metaText = () => agentMeta()
|
||||
const metaText = () => ""
|
||||
|
||||
const updateMetaLayout = () => {
|
||||
const text = metaText()
|
||||
|
|
@ -142,7 +143,6 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
const isUser = () => props.record.role === "user"
|
||||
const createdTimestamp = () => getMessageStartedAt(props.messageInfo, props.record.createdAt) ?? props.record.createdAt
|
||||
const totalDuration = () => getMessageDurationMs(props.messageInfo, props.record.status, props.record.createdAt)
|
||||
const totalDurationLabel = () => (!isUser() ? formatElapsedClock(totalDuration(), locale()) : "")
|
||||
|
||||
const timestamp = () => {
|
||||
const date = new Date(createdTimestamp())
|
||||
|
|
@ -158,6 +158,11 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
}
|
||||
|
||||
const messageParts = () => props.parts
|
||||
const isAssistantTextBlock = () =>
|
||||
!isUser() &&
|
||||
messageParts().some(
|
||||
(part) => part.type === "text" && !isHiddenSyntheticTextPart(part) && partHasRenderableText(part),
|
||||
)
|
||||
|
||||
// User messages can temporarily include synthetic helper parts (e.g. tool traces / file reads).
|
||||
// We only want to display the primary prompt text for the user message; other synthetic text
|
||||
|
|
@ -354,9 +359,7 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
const containerClass = () =>
|
||||
isUser()
|
||||
? "message-item-base bg-[var(--message-user-bg)] border-l-4 border-[var(--message-user-border)]"
|
||||
: "message-item-base assistant-message bg-[var(--message-assistant-bg)] border-l-4 border-[var(--message-assistant-border)]"
|
||||
|
||||
const speakerLabel = () => (isUser() ? t("messageItem.speaker.you") : t("messageItem.speaker.assistant"))
|
||||
: "message-item-base assistant-message border-l-4 border-[var(--message-assistant-border)]"
|
||||
|
||||
const agentIdentifier = () => {
|
||||
if (isUser()) return ""
|
||||
|
|
@ -397,23 +400,66 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
return segments.join(" • ")
|
||||
}
|
||||
|
||||
const actionMenuItems = (): ActionOverflowMenuItem[] => {
|
||||
const items: ActionOverflowMenuItem[] = [
|
||||
{
|
||||
const speakerLabel = () => {
|
||||
if (isUser()) return t("messageItem.speaker.you")
|
||||
return agentMeta() || t("messageItem.speaker.assistant")
|
||||
}
|
||||
|
||||
const workedDurationTooltip = () => {
|
||||
if (isUser()) return ""
|
||||
const duration = totalDuration()
|
||||
if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0) return ""
|
||||
|
||||
const formatWorkedFor = (count: number, unit: "seconds" | "minutes" | "hours") =>
|
||||
t(`messageItem.duration.workedFor.${unit}.${count === 1 ? "one" : "other"}`, { count })
|
||||
|
||||
const seconds = Math.max(1, Math.round(duration / 1000))
|
||||
if (seconds < 60) {
|
||||
return formatWorkedFor(seconds, "seconds")
|
||||
}
|
||||
|
||||
const minutes = Math.max(1, Math.round(seconds / 60))
|
||||
if (minutes < 60) {
|
||||
return formatWorkedFor(minutes, "minutes")
|
||||
}
|
||||
|
||||
const hours = Math.max(1, Math.round(minutes / 60))
|
||||
return formatWorkedFor(hours, "hours")
|
||||
}
|
||||
|
||||
const actionMenuItems = (includePrimaryActions = false): ActionOverflowMenuItem[] => {
|
||||
const items: ActionOverflowMenuItem[] = []
|
||||
|
||||
if (props.showDeleteMessage) {
|
||||
items.push({
|
||||
key: "select",
|
||||
label: isSelectedForDeletion()
|
||||
? t("messageItem.selection.deselectForDeletion")
|
||||
: t("messageItem.selection.selectForDeletion"),
|
||||
icon: isSelectedForDeletion()
|
||||
? <CheckSquare2 class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
: <Square class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
checked: isSelectedForDeletion(),
|
||||
onSelect: () => props.onToggleSelectedMessage?.(props.record.id, !isSelectedForDeletion()),
|
||||
})
|
||||
}
|
||||
|
||||
if (includePrimaryActions) {
|
||||
items.push({
|
||||
key: "copy",
|
||||
label: copyLabel(),
|
||||
icon: <Copy class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: handleCopy,
|
||||
},
|
||||
]
|
||||
|
||||
if (canSpeakMessage()) {
|
||||
items.push({
|
||||
key: "speak",
|
||||
label: speech.buttonTitle(),
|
||||
icon: <Volume2 class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => void speech.toggle(),
|
||||
})
|
||||
|
||||
if (canSpeakMessage()) {
|
||||
items.push({
|
||||
key: "speak",
|
||||
label: speech.buttonTitle(),
|
||||
icon: <Volume2 class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => void speech.toggle(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isUser() && props.onFork) {
|
||||
|
|
@ -472,35 +518,17 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
data-message-id={props.record.id}
|
||||
data-message-role={isUser() ? "user" : "assistant"}
|
||||
data-message-status={props.record.status}
|
||||
data-message-content-start-part-id={props.contentStartPartId}
|
||||
data-assistant-text-block={isAssistantTextBlock() ? "true" : undefined}
|
||||
>
|
||||
<header class={`message-item-header ${isUser() ? "pb-0.5" : "pb-0"}`}>
|
||||
<header class="message-item-header pb-0">
|
||||
<div class="message-item-header-row message-item-header-row--top" ref={(el) => (topRowEl = el)}>
|
||||
<div class="message-header-left">
|
||||
<div class="message-speaker-primary" ref={(el) => (speakerPrimaryEl = el)}>
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<input
|
||||
class="message-select-checkbox"
|
||||
type="checkbox"
|
||||
checked={isSelectedForDeletion()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||
props.onToggleSelectedMessage?.(props.record.id, next)
|
||||
}}
|
||||
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<span class="message-speaker-label" data-role={isUser() ? "user" : "assistant"}>
|
||||
<span class="message-speaker-label" data-role={isUser() ? "user" : "assistant"} title={workedDurationTooltip() || undefined}>
|
||||
{speakerLabel()}
|
||||
</span>
|
||||
<Show when={totalDurationLabel()}>
|
||||
{(value) => <span class="message-duration">{value()}</span>}
|
||||
</Show>
|
||||
<time class="message-timestamp message-timestamp-inline" dateTime={timestampIso()}>{timestamp()}</time>
|
||||
</div>
|
||||
|
||||
<Show when={metaText() && showMetaInline()}>
|
||||
|
|
@ -519,7 +547,7 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
|
||||
<div
|
||||
class="message-item-actions"
|
||||
data-action-overflow={actionMenuItems().length > 1 ? "true" : undefined}
|
||||
data-action-overflow={actionMenuItems(true).length > 0 ? "true" : undefined}
|
||||
ref={(el) => (actionsEl = el)}
|
||||
>
|
||||
<Show when={isUser()}>
|
||||
|
|
@ -543,59 +571,18 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={props.onFork}>
|
||||
<button
|
||||
class="message-action-button"
|
||||
onClick={() => props.onFork?.(props.record.id)}
|
||||
title={t("messageItem.actions.fork")}
|
||||
aria-label={t("messageItem.actions.fork")}
|
||||
>
|
||||
<Split class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
<Show when={props.onRevert}>
|
||||
<button
|
||||
class="message-action-button"
|
||||
onClick={handleRevert}
|
||||
title={t("messageItem.actions.revertTitle")}
|
||||
aria-label={t("messageItem.actions.revertTitle")}
|
||||
>
|
||||
<Undo class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<button
|
||||
class="message-action-button"
|
||||
onClick={() => void handleDeleteUpTo()}
|
||||
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
|
||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.record.id })}
|
||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||
title={t("messageItem.actions.deleteMessagesUpTo")}
|
||||
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
|
||||
>
|
||||
<DeleteUpToIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="message-action-button"
|
||||
onClick={handleDeleteMessage}
|
||||
disabled={deletingMessage()}
|
||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.record.id })}
|
||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
>
|
||||
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button"
|
||||
minItems={2}
|
||||
triggerClass="message-action-button action-overflow-wide"
|
||||
minItems={1}
|
||||
/>
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems(true)}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button action-overflow-narrow"
|
||||
minItems={1}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={!isUser()}>
|
||||
|
|
@ -619,42 +606,20 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<button
|
||||
class="message-action-button"
|
||||
onClick={() => void handleDeleteUpTo()}
|
||||
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
|
||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.record.id })}
|
||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||
title={t("messageItem.actions.deleteMessagesUpTo")}
|
||||
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
|
||||
>
|
||||
<DeleteUpToIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="message-action-button"
|
||||
onClick={handleDeleteMessage}
|
||||
disabled={deletingMessage()}
|
||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.record.id })}
|
||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
>
|
||||
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button"
|
||||
minItems={2}
|
||||
triggerClass="message-action-button action-overflow-wide"
|
||||
minItems={1}
|
||||
/>
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems(true)}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="message-action-button action-overflow-narrow"
|
||||
minItems={1}
|
||||
/>
|
||||
</Show>
|
||||
<div class="message-meta-timing">
|
||||
<time class="message-timestamp" dateTime={timestampIso()}>{timestamp()}</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -666,7 +631,7 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
|
||||
</header>
|
||||
|
||||
<div class="pt-1 whitespace-pre-wrap break-words leading-[1.1]" dir="auto">
|
||||
<div class="pt-0 whitespace-pre-wrap break-words leading-[1.1]" dir="auto">
|
||||
|
||||
|
||||
<Show when={props.isQueued && isUser()}>
|
||||
|
|
|
|||
|
|
@ -36,10 +36,17 @@ export default function MessagePart(props: MessagePartProps) {
|
|||
const textContainerClass = () => (isAssistantMessage() ? "message-text message-text-assistant" : "message-text")
|
||||
const markdownContainerClass = () => "message-text message-text-assistant"
|
||||
const textContainerRole = () => props.messageType || "assistant"
|
||||
const isPrimaryUserTextPart = () =>
|
||||
props.messageType === "user" &&
|
||||
props.part?.type === "text" &&
|
||||
typeof props.primaryUserTextPartId === "string" &&
|
||||
props.primaryUserTextPartId.length > 0 &&
|
||||
props.part.id === props.primaryUserTextPartId
|
||||
|
||||
const shouldHideTextPart = () => {
|
||||
const part = props.part
|
||||
if (!part || part.type !== "text") return false
|
||||
if (isPrimaryUserTextPart()) return false
|
||||
return Boolean((part as any).synthetic)
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +220,7 @@ export default function MessagePart(props: MessagePartProps) {
|
|||
return (
|
||||
<Switch>
|
||||
<Match when={partType() === "text"}>
|
||||
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
|
||||
<Show when={!shouldHideTextPart() && (partHasRenderableText(props.part) || isPrimaryUserTextPart())}>
|
||||
<div
|
||||
class={canRenderMarkdown() ? markdownContainerClass() : textContainerClass()}
|
||||
dir="auto"
|
||||
|
|
@ -270,6 +277,7 @@ export default function MessagePart(props: MessagePartProps) {
|
|||
toolCallId={props.part?.id}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
onContentRendered={props.onRendered}
|
||||
/>
|
||||
</Suspense>
|
||||
</Match>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { Show, createEffect, createMemo, createSignal, onCleanup, on, untrack } from "solid-js"
|
||||
import { ChevronDown, ChevronUp, MoreHorizontal, Pause, Search, Trash, X } from "lucide-solid"
|
||||
import { ArrowUpDown, ChevronDown, ChevronUp, MoreHorizontal, Pause, Search, Trash, X } from "lucide-solid"
|
||||
import Kbd from "./kbd"
|
||||
import BrandedEmptyState from "./branded-empty-state"
|
||||
import MessageBlock from "./message-block"
|
||||
import { getMessageAnchorId } from "./message-anchors"
|
||||
import MessageTimeline, { buildTimelineSegments, type TimelineSegment } from "./message-timeline"
|
||||
import VirtualFollowList, { type VirtualFollowListApi, type VirtualFollowListState, type VirtualFollowScrollSnapshot } from "./virtual-follow-list"
|
||||
import VirtualFollowList, { type VirtualFollowBottomIntent, type VirtualFollowListApi, type VirtualFollowListState, type VirtualFollowScrollSnapshot } from "./virtual-follow-list"
|
||||
import { isSnapshotAutoFollowing } from "./virtual-follow-behavior"
|
||||
import { useConfig } from "../stores/preferences"
|
||||
import { getSessionInfo } from "../stores/sessions"
|
||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||
|
|
@ -21,6 +22,7 @@ import { buildRecordDisplayData } from "../stores/message-v2/record-display-cach
|
|||
import { getPartCharCount } from "../lib/token-utils"
|
||||
import { buildSessionSearchMatches } from "../lib/session-search"
|
||||
import type { SessionSearchMatch } from "../lib/session-search"
|
||||
import { resolveThinkingExpansionDefault } from "./tool-call/tool-registry"
|
||||
|
||||
const SCROLL_SENTINEL_MARGIN_PX = 8
|
||||
const MESSAGE_SCROLL_CACHE_SCOPE = "message-stream"
|
||||
|
|
@ -47,6 +49,7 @@ export interface MessageSectionProps {
|
|||
onReloadMessages?: () => void
|
||||
isActive?: boolean
|
||||
sessionStreamingActive?: boolean
|
||||
bottomFollowIntent?: VirtualFollowBottomIntent | null
|
||||
}
|
||||
|
||||
export default function MessageSection(props: MessageSectionProps) {
|
||||
|
|
@ -115,7 +118,7 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
const preferenceSignature = createMemo(() => {
|
||||
const pref = preferences()
|
||||
const showThinking = pref.showThinkingBlocks ? 1 : 0
|
||||
const thinkingExpansion = pref.thinkingBlocksExpansion ?? "expanded"
|
||||
const thinkingExpansion = resolveThinkingExpansionDefault(pref) ? "expanded" : "collapsed"
|
||||
const showUsage = (pref.showUsageMetrics ?? true) ? 1 : 0
|
||||
return `${showThinking}|${thinkingExpansion}|${showUsage}`
|
||||
})
|
||||
|
|
@ -646,10 +649,13 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
const isActive = createMemo(() => props.isActive !== false)
|
||||
const [listApi, setListApi] = createSignal<VirtualFollowListApi | null>(null)
|
||||
const [listState, setListState] = createSignal<VirtualFollowListState | null>(null)
|
||||
const [scrollControlsOpen, setScrollControlsOpen] = createSignal(false)
|
||||
const [scrollControlsHoverSuppressed, setScrollControlsHoverSuppressed] = createSignal(false)
|
||||
const scrollButtonsCount = createMemo(() => listState()?.scrollButtonsCount() ?? 0)
|
||||
|
||||
const [streamElement, setStreamElement] = createSignal<HTMLDivElement | undefined>()
|
||||
const [streamShellElement, setStreamShellElement] = createSignal<HTMLDivElement | undefined>()
|
||||
let scrollControlsRef: HTMLDivElement | undefined
|
||||
|
||||
// Only preferences should force a follow-token re-anchor. Message/session
|
||||
// revision churn at the end of a turn (message.updated, session.idle, etc.)
|
||||
|
|
@ -657,7 +663,7 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
const followToken = createMemo(() => preferenceSignature())
|
||||
|
||||
const initialScrollSnapshot = createMemo(() => store().getScrollSnapshot(props.sessionId, MESSAGE_SCROLL_CACHE_SCOPE))
|
||||
const initialAutoScroll = createMemo(() => initialScrollSnapshot()?.atBottom ?? true)
|
||||
const initialAutoScroll = createMemo(() => isSnapshotAutoFollowing(initialScrollSnapshot()))
|
||||
|
||||
const [didRestoreScroll, setDidRestoreScroll] = createSignal(false)
|
||||
const lastGoodScrollSnapshots = new Map<string, VirtualFollowScrollSnapshot>()
|
||||
|
|
@ -773,6 +779,40 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
updatePreferences({ holdLongAssistantReplies: !holdLongAssistantRepliesEnabled() })
|
||||
}
|
||||
|
||||
function closeScrollControls() {
|
||||
setScrollControlsOpen(false)
|
||||
}
|
||||
|
||||
function openScrollControlsFromTrigger(event: PointerEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (scrollControlsOpen()) return
|
||||
setScrollControlsHoverSuppressed(false)
|
||||
setScrollControlsOpen(true)
|
||||
}
|
||||
|
||||
function runScrollControlAction(event: PointerEvent, action: () => void) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
action()
|
||||
setScrollControlsHoverSuppressed(event.pointerType !== "mouse")
|
||||
closeScrollControls()
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!scrollControlsOpen()) return
|
||||
if (typeof document === "undefined") return
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
const target = event.target as Node | null
|
||||
if (target && scrollControlsRef?.contains(target)) return
|
||||
closeScrollControls()
|
||||
}
|
||||
|
||||
document.addEventListener("pointerdown", handlePointerDown)
|
||||
onCleanup(() => document.removeEventListener("pointerdown", handlePointerDown))
|
||||
})
|
||||
|
||||
function isStreamingAssistantTextMessage(messageId: string | null | undefined) {
|
||||
if (!messageId) return false
|
||||
const resolvedStore = store()
|
||||
|
|
@ -829,8 +869,6 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
setDidRestoreScroll(true)
|
||||
},
|
||||
onApplied: () => {
|
||||
// Keep follow mode consistent with the restored state.
|
||||
api.setAutoScroll(snapshot.atBottom)
|
||||
restoringScrollSnapshot = false
|
||||
setLastGoodScrollSnapshot(props.sessionId, snapshot)
|
||||
setDidRestoreScroll(true)
|
||||
|
|
@ -1338,10 +1376,12 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
initialAutoScroll={initialAutoScroll}
|
||||
resetKey={() => props.sessionId}
|
||||
followToken={followToken}
|
||||
forceBottomFollowIntent={() => props.bottomFollowIntent ?? null}
|
||||
autoPinHoldEnabled={holdLongAssistantRepliesEnabled}
|
||||
autoPinHoldTargetKey={autoPinHoldTargetKey}
|
||||
autoPinHoldTopThresholdPx={STREAMING_TEXT_HOLD_TOP_THRESHOLD_PX}
|
||||
resolveAutoPinHoldElement={(itemWrapper, key) => {
|
||||
const candidates = Array.from(itemWrapper.querySelectorAll<HTMLElement>(`.message-item-base[data-message-id="${key}"][data-message-role="assistant"]`))
|
||||
const candidates = Array.from(itemWrapper.querySelectorAll<HTMLElement>(`.message-item-base[data-message-id="${key}"][data-message-role="assistant"][data-assistant-text-block="true"]`))
|
||||
return candidates[candidates.length - 1] ?? null
|
||||
}}
|
||||
onScroll={() => {
|
||||
|
|
@ -1375,50 +1415,72 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
registerApi={(api) => setListApi(api)}
|
||||
registerState={(state) => setListState(state)}
|
||||
renderControls={(state, api) => (
|
||||
<div class="message-scroll-button-wrapper">
|
||||
<div
|
||||
ref={(el) => {
|
||||
scrollControlsRef = el
|
||||
}}
|
||||
class="message-scroll-controls"
|
||||
data-open={scrollControlsOpen() ? "true" : "false"}
|
||||
data-hover-suppressed={scrollControlsHoverSuppressed() ? "true" : "false"}
|
||||
onPointerLeave={(event) => {
|
||||
if (event.pointerType === "mouse") setScrollControlsHoverSuppressed(false)
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="message-scroll-button"
|
||||
data-active={holdLongAssistantRepliesEnabled() ? "true" : "false"}
|
||||
onClick={toggleHoldLongAssistantReplies}
|
||||
aria-pressed={holdLongAssistantRepliesEnabled()}
|
||||
aria-label={
|
||||
holdLongAssistantRepliesEnabled()
|
||||
? t("messageSection.scroll.disableHoldAriaLabel")
|
||||
: t("messageSection.scroll.enableHoldAriaLabel")
|
||||
}
|
||||
title={
|
||||
holdLongAssistantRepliesEnabled()
|
||||
? t("messageSection.scroll.disableHoldAriaLabel")
|
||||
: t("messageSection.scroll.enableHoldAriaLabel")
|
||||
}
|
||||
class="message-scroll-button message-scroll-controls-trigger"
|
||||
onPointerUp={openScrollControlsFromTrigger}
|
||||
aria-label={t("messageSection.scroll.showControlsAriaLabel")}
|
||||
title={t("messageSection.scroll.showControlsAriaLabel")}
|
||||
>
|
||||
<Pause class="message-scroll-icon message-scroll-icon--toggle w-4 h-4" aria-hidden="true" />
|
||||
<ArrowUpDown class="message-scroll-icon w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
<Show when={state.showScrollTopButton()}>
|
||||
|
||||
<div class="message-scroll-controls-expanded">
|
||||
<button
|
||||
type="button"
|
||||
class="message-scroll-button"
|
||||
onClick={() => api.scrollToTop()}
|
||||
aria-label={t("messageSection.scroll.toFirstAriaLabel")}
|
||||
data-active={holdLongAssistantRepliesEnabled() ? "true" : "false"}
|
||||
onPointerUp={(event) => runScrollControlAction(event, toggleHoldLongAssistantReplies)}
|
||||
aria-pressed={holdLongAssistantRepliesEnabled()}
|
||||
aria-label={
|
||||
holdLongAssistantRepliesEnabled()
|
||||
? t("messageSection.scroll.disableHoldAriaLabel")
|
||||
: t("messageSection.scroll.enableHoldAriaLabel")
|
||||
}
|
||||
title={
|
||||
holdLongAssistantRepliesEnabled()
|
||||
? t("messageSection.scroll.disableHoldAriaLabel")
|
||||
: t("messageSection.scroll.enableHoldAriaLabel")
|
||||
}
|
||||
>
|
||||
<span class="message-scroll-icon" aria-hidden="true">
|
||||
↑
|
||||
</span>
|
||||
<Pause class="message-scroll-icon message-scroll-icon--toggle w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={state.showScrollBottomButton()}>
|
||||
<button
|
||||
type="button"
|
||||
class="message-scroll-button"
|
||||
onClick={() => api.scrollToBottom({ suppressHold: true })}
|
||||
aria-label={t("messageSection.scroll.toLatestAriaLabel")}
|
||||
>
|
||||
<span class="message-scroll-icon" aria-hidden="true">
|
||||
↓
|
||||
</span>
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={state.showScrollTopButton()}>
|
||||
<button
|
||||
type="button"
|
||||
class="message-scroll-button"
|
||||
onPointerUp={(event) => runScrollControlAction(event, () => api.scrollToTop())}
|
||||
aria-label={t("messageSection.scroll.toFirstAriaLabel")}
|
||||
>
|
||||
<span class="message-scroll-icon" aria-hidden="true">
|
||||
↑
|
||||
</span>
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={state.showScrollBottomButton()}>
|
||||
<button
|
||||
type="button"
|
||||
class="message-scroll-button"
|
||||
onPointerUp={(event) => runScrollControlAction(event, () => api.scrollToBottom({ suppressHold: true }))}
|
||||
aria-label={t("messageSection.scroll.toLatestAriaLabel")}
|
||||
>
|
||||
<span class="message-scroll-icon" aria-hidden="true">
|
||||
↓
|
||||
</span>
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
renderBeforeItems={() => (
|
||||
|
|
@ -1493,7 +1555,7 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
messageIndex={index}
|
||||
lastAssistantIndex={lastAssistantIndex}
|
||||
showThinking={() => preferences().showThinkingBlocks}
|
||||
thinkingDefaultExpanded={() => (preferences().thinkingBlocksExpansion ?? "expanded") === "expanded"}
|
||||
thinkingDefaultExpanded={() => resolveThinkingExpansionDefault(preferences())}
|
||||
showUsageMetrics={showUsagePreference}
|
||||
deleteHover={deleteHover}
|
||||
onDeleteHoverChange={setDeleteHover}
|
||||
|
|
|
|||
|
|
@ -138,8 +138,18 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
|
|||
const [loadingSession, setLoadingSession] = createSignal<string | null>(null)
|
||||
const [permissionSubmitting, setPermissionSubmitting] = createSignal<Set<string>>(new Set())
|
||||
const [permissionError, setPermissionError] = createSignal<Map<string, string>>(new Map())
|
||||
const [rejectingPermissionId, setRejectingPermissionId] = createSignal<string | null>(null)
|
||||
const [rejectReason, setRejectReason] = createSignal("")
|
||||
const [permissionRejectReasons, setPermissionRejectReasons] = createSignal<Map<string, string>>(new Map())
|
||||
|
||||
const getPermissionRejectReason = (permissionId: string) => permissionRejectReasons().get(permissionId) ?? ""
|
||||
|
||||
const setPermissionRejectReason = (permissionId: string, value: string) => {
|
||||
setPermissionRejectReasons((prev) => {
|
||||
const next = new Map(prev)
|
||||
if (value.length === 0) next.delete(permissionId)
|
||||
else next.set(permissionId, value)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const setPermissionBusy = (permissionId: string, busy: boolean) => {
|
||||
setPermissionSubmitting((prev) => {
|
||||
|
|
@ -172,10 +182,7 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
|
|||
const sessionId = getPermissionSessionId(permission)
|
||||
if (!sessionId) throw new Error("Permission request is missing sessionID")
|
||||
await sendPermissionResponse(props.instanceId, sessionId, permissionId, response, message)
|
||||
if (rejectingPermissionId() === permissionId) {
|
||||
setRejectingPermissionId(null)
|
||||
setRejectReason("")
|
||||
}
|
||||
setPermissionRejectReason(permissionId, "")
|
||||
} catch (error) {
|
||||
setPermissionItemError(
|
||||
permissionId,
|
||||
|
|
@ -379,86 +386,53 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
|
|||
<code>{primaryTitle()}</code>
|
||||
</div>
|
||||
<Show when={item.kind === "permission"}>
|
||||
<Show
|
||||
when={rejectingPermissionId() === item.id}
|
||||
fallback={
|
||||
<div class="tool-call-permission-actions">
|
||||
<div class="tool-call-permission-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={permissionSubmitting().has(item.id)}
|
||||
onClick={() => void handlePermissionDecision(item.payload as PermissionRequest, "once")}
|
||||
>
|
||||
{t("permissionApproval.actions.allowOnce")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={permissionSubmitting().has(item.id)}
|
||||
onClick={() => void handlePermissionDecision(item.payload as PermissionRequest, "always")}
|
||||
>
|
||||
{t("permissionApproval.actions.alwaysAllow")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={permissionSubmitting().has(item.id)}
|
||||
onClick={() => {
|
||||
setRejectingPermissionId(item.id)
|
||||
setRejectReason("")
|
||||
}}
|
||||
>
|
||||
{t("permissionApproval.actions.deny")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="tool-call-permission-reject-reason">
|
||||
<label class="tool-call-permission-reject-label" for={`permission-center-reject-reason-${item.id}`}>
|
||||
{t("permissionApproval.rejectReason.label")}
|
||||
</label>
|
||||
<textarea
|
||||
id={`permission-center-reject-reason-${item.id}`}
|
||||
class="tool-call-permission-reject-textarea"
|
||||
value={rejectReason()}
|
||||
rows={3}
|
||||
maxLength={PERMISSION_REJECT_REASON_MAX_LENGTH}
|
||||
placeholder={t("permissionApproval.rejectReason.placeholder")}
|
||||
<div class="tool-call-permission-reject-reason">
|
||||
<textarea
|
||||
id={`permission-center-reject-reason-${item.id}`}
|
||||
class="tool-call-permission-reject-textarea"
|
||||
value={getPermissionRejectReason(item.id)}
|
||||
rows={1}
|
||||
maxLength={PERMISSION_REJECT_REASON_MAX_LENGTH}
|
||||
placeholder={t("permissionApproval.rejectReason.placeholder")}
|
||||
aria-label={t("permissionApproval.rejectReason.placeholder")}
|
||||
disabled={permissionSubmitting().has(item.id)}
|
||||
onInput={(event) => setPermissionRejectReason(item.id, event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="tool-call-permission-actions">
|
||||
<div class="tool-call-permission-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={permissionSubmitting().has(item.id)}
|
||||
onInput={(event) => setRejectReason(event.currentTarget.value)}
|
||||
/>
|
||||
<p class="tool-call-permission-reject-hint">{t("permissionApproval.rejectReason.hint")}</p>
|
||||
<div class="tool-call-permission-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={permissionSubmitting().has(item.id)}
|
||||
onClick={() =>
|
||||
void handlePermissionDecision(
|
||||
item.payload as PermissionRequest,
|
||||
"reject",
|
||||
rejectReason().trim() || undefined,
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("permissionApproval.actions.confirmDeny")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={permissionSubmitting().has(item.id)}
|
||||
onClick={() => {
|
||||
setRejectingPermissionId(null)
|
||||
setRejectReason("")
|
||||
}}
|
||||
>
|
||||
{t("permissionApproval.actions.cancel")}
|
||||
</button>
|
||||
</div>
|
||||
onClick={() => void handlePermissionDecision(item.payload as PermissionRequest, "once")}
|
||||
>
|
||||
{t("permissionApproval.actions.allowOnce")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={permissionSubmitting().has(item.id)}
|
||||
onClick={() => void handlePermissionDecision(item.payload as PermissionRequest, "always")}
|
||||
>
|
||||
{t("permissionApproval.actions.alwaysAllow")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={permissionSubmitting().has(item.id)}
|
||||
onClick={() =>
|
||||
void handlePermissionDecision(
|
||||
item.payload as PermissionRequest,
|
||||
"reject",
|
||||
getPermissionRejectReason(item.id).trim() || undefined,
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("permissionApproval.actions.deny")}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={permissionError().get(item.id)}>
|
||||
{(err) => <div class="tool-call-permission-error">{err()}</div>}
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Suspense, createEffect, createSignal, lazy, on, onCleanup, Show } from "solid-js"
|
||||
import { Suspense, createEffect, createSignal, lazy, on, onCleanup, onMount, Show } from "solid-js"
|
||||
import { ArrowBigUp, ArrowBigDown, Loader2, Mic, Paperclip, Volume2, X } from "lucide-solid"
|
||||
import ExpandButton from "./expand-button"
|
||||
import { clearAttachments, removeAttachment } from "../stores/attachments"
|
||||
|
|
@ -33,6 +33,13 @@ const log = getLogger("actions")
|
|||
const LazyUnifiedPicker = lazy(() => import("./unified-picker"))
|
||||
const DEFAULT_PROMPT_FIELD_HEIGHT = 104
|
||||
const MAX_PROMPT_FIELD_HEIGHT_RATIO = 0.6
|
||||
type SessionCenterWidthStep = "narrow" | "medium" | "wide"
|
||||
|
||||
function getSessionCenterWidthStep(width: number): SessionCenterWidthStep {
|
||||
if (width < 768) return "narrow"
|
||||
if (width < 1280) return "medium"
|
||||
return "wide"
|
||||
}
|
||||
|
||||
type ResizeDragState = {
|
||||
pointerId: number
|
||||
|
|
@ -75,7 +82,9 @@ export default function PromptInput(props: PromptInputProps) {
|
|||
const [mode, setMode] = createSignal<PromptMode>("normal")
|
||||
const [expandState, setExpandState] = createSignal<ExpandState>("normal")
|
||||
const [inputHeight, setInputHeight] = createSignal<number | null>(null)
|
||||
const [autoInputHeight, setAutoInputHeight] = createSignal<number | null>(null)
|
||||
const [isResizing, setIsResizing] = createSignal(false)
|
||||
const [sessionCenterWidthStep, setSessionCenterWidthStep] = createSignal<SessionCenterWidthStep | null>(null)
|
||||
const [isFileBrowserOpen, setIsFileBrowserOpen] = createSignal(false)
|
||||
const SELECTION_INSERT_MAX_LENGTH = 2000
|
||||
const MAX_READABLE_PICKED_FILE_BYTES = 5 * 1024 * 1024
|
||||
|
|
@ -92,6 +101,54 @@ export default function PromptInput(props: PromptInputProps) {
|
|||
return t("promptInput.placeholder.default")
|
||||
}
|
||||
|
||||
const compactAutosizeEnabled = () => {
|
||||
const widthStep = sessionCenterWidthStep()
|
||||
return props.compactLayout && expandState() === "normal" && inputHeight() === null && widthStep === "narrow"
|
||||
}
|
||||
|
||||
const effectiveInputHeight = () => inputHeight() ?? autoInputHeight()
|
||||
|
||||
const fieldHeightStyle = () => {
|
||||
const height = effectiveInputHeight()
|
||||
if (height === null) return undefined
|
||||
if (inputHeight() !== null) return { height: `${height}px`, "min-height": `${height}px` }
|
||||
return { height: `${height}px` }
|
||||
}
|
||||
|
||||
const textareaHeightStyle = () => {
|
||||
const height = effectiveInputHeight()
|
||||
if (height === null) return undefined
|
||||
const overflowY: "auto" | "hidden" = inputHeight() !== null || height >= DEFAULT_PROMPT_FIELD_HEIGHT ? "auto" : "hidden"
|
||||
if (inputHeight() !== null) {
|
||||
return {
|
||||
height: `${height}px`,
|
||||
"min-height": `${height}px`,
|
||||
"overflow-y": overflowY,
|
||||
}
|
||||
}
|
||||
return { height: `${height}px`, "overflow-y": overflowY }
|
||||
}
|
||||
|
||||
const textareaRows = () => {
|
||||
if (expandState() === "expanded") return props.compactLayout ? 10 : 15
|
||||
return compactAutosizeEnabled() ? 2 : 3
|
||||
}
|
||||
|
||||
const syncCompactAutoHeight = () => {
|
||||
const textarea = textareaRef
|
||||
if (!textarea || !compactAutosizeEnabled()) {
|
||||
setAutoInputHeight(null)
|
||||
return
|
||||
}
|
||||
|
||||
const previousHeight = textarea.style.height
|
||||
textarea.style.height = "auto"
|
||||
const measuredHeight = textarea.scrollHeight
|
||||
textarea.style.height = previousHeight
|
||||
const nextHeight = Math.min(DEFAULT_PROMPT_FIELD_HEIGHT, measuredHeight)
|
||||
setAutoInputHeight(nextHeight)
|
||||
}
|
||||
|
||||
const promptState = usePromptState({
|
||||
instanceId: () => props.instanceId,
|
||||
sessionId: () => props.sessionId,
|
||||
|
|
@ -112,6 +169,31 @@ export default function PromptInput(props: PromptInputProps) {
|
|||
selectNextHistory,
|
||||
} = promptState
|
||||
|
||||
onMount(() => {
|
||||
const sessionCenter = wrapperRef?.closest("[data-session-center-width]") as HTMLElement | null
|
||||
if (!sessionCenter) return
|
||||
|
||||
const syncWidthStep = () => {
|
||||
setSessionCenterWidthStep(getSessionCenterWidthStep(sessionCenter.getBoundingClientRect().width))
|
||||
}
|
||||
|
||||
syncWidthStep()
|
||||
|
||||
if (typeof ResizeObserver === "undefined") return
|
||||
const observer = new ResizeObserver(syncWidthStep)
|
||||
observer.observe(sessionCenter)
|
||||
onCleanup(() => observer.disconnect())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
prompt()
|
||||
expandState()
|
||||
inputHeight()
|
||||
props.compactLayout
|
||||
sessionCenterWidthStep()
|
||||
queueMicrotask(syncCompactAutoHeight)
|
||||
})
|
||||
|
||||
const {
|
||||
attachments,
|
||||
isDragging,
|
||||
|
|
@ -248,6 +330,11 @@ export default function PromptInput(props: PromptInputProps) {
|
|||
return Boolean(window.matchMedia?.("(pointer: coarse)")?.matches)
|
||||
}
|
||||
|
||||
const isTouchOnlyPointer = () => {
|
||||
if (typeof window === "undefined") return false
|
||||
return Boolean(window.matchMedia?.("(pointer: coarse)")?.matches && !window.matchMedia?.("(any-pointer: fine)")?.matches)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
// Scope global "type-to-focus" behavior to the active, visible prompt only.
|
||||
if (typeof document === "undefined") return
|
||||
|
|
@ -450,7 +537,9 @@ export default function PromptInput(props: PromptInputProps) {
|
|||
variant: "error",
|
||||
})
|
||||
} finally {
|
||||
textareaRef?.focus()
|
||||
if (!isTouchOnlyPointer()) {
|
||||
textareaRef?.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -703,6 +792,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||
<div
|
||||
ref={wrapperRef}
|
||||
class={`prompt-input-wrapper relative ${isDragging() ? "border-2" : ""}`}
|
||||
data-compact-layout={props.compactLayout ? "true" : undefined}
|
||||
style={
|
||||
isDragging()
|
||||
? "border-color: var(--accent-primary); background-color: rgba(0, 102, 255, 0.05);"
|
||||
|
|
@ -736,8 +826,8 @@ export default function PromptInput(props: PromptInputProps) {
|
|||
<div class="prompt-input-main flex flex-1 flex-col">
|
||||
<div
|
||||
ref={fieldContainerRef}
|
||||
class={`prompt-input-field-container ${expandState() === "expanded" ? "is-expanded" : ""} ${inputHeight() !== null ? "is-resized" : ""}`}
|
||||
style={inputHeight() !== null ? { height: `${inputHeight()}px`, "min-height": `${inputHeight()}px` } : undefined}
|
||||
class={`prompt-input-field-container ${expandState() === "expanded" ? "is-expanded" : ""} ${effectiveInputHeight() !== null ? "is-resized" : ""}`}
|
||||
style={fieldHeightStyle()}
|
||||
>
|
||||
<div
|
||||
class={`prompt-resize-handle ${isResizing() ? "is-resizing" : ""}`}
|
||||
|
|
@ -752,7 +842,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||
|
||||
<div
|
||||
class={`prompt-input-field ${expandState() === "expanded" ? "is-expanded" : ""}`}
|
||||
style={inputHeight() !== null ? { height: `${inputHeight()}px`, "min-height": `${inputHeight()}px` } : undefined}
|
||||
style={fieldHeightStyle()}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
|
|
@ -766,12 +856,12 @@ export default function PromptInput(props: PromptInputProps) {
|
|||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
disabled={props.disabled}
|
||||
rows={expandState() === "expanded" ? (props.compactLayout ? 10 : 15) : 3}
|
||||
rows={textareaRows()}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autoCapitalize="off"
|
||||
autocomplete="off"
|
||||
style={inputHeight() !== null ? { height: `${inputHeight()}px`, "min-height": `${inputHeight()}px`, "overflow-y": "auto" } : undefined}
|
||||
style={textareaHeightStyle()}
|
||||
/>
|
||||
<div class="prompt-expand-button-inline">
|
||||
<ExpandButton
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
|
|||
}
|
||||
|
||||
async function handleCancel() {
|
||||
await stopInstance(props.instanceId)
|
||||
stopInstance(props.instanceId)
|
||||
props.onClose()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { describe, it } from "node:test"
|
||||
|
||||
import { resolveSessionBottomFollowIntent, shouldClearSessionBottomFollowIntent } from "./session-bottom-follow-intent.ts"
|
||||
|
||||
describe("session bottom follow intent", () => {
|
||||
it("only exposes submit follow intent to the matching session", () => {
|
||||
const intent = { sessionId: "session-a", token: 3, minItemCount: 12 }
|
||||
|
||||
assert.deepEqual(resolveSessionBottomFollowIntent(intent, "session-a"), {
|
||||
token: 3,
|
||||
minItemCount: 12,
|
||||
})
|
||||
assert.equal(resolveSessionBottomFollowIntent(intent, "session-b"), null)
|
||||
})
|
||||
|
||||
it("clears submit follow intent only after the submitted exchange has rendered and stopped streaming", () => {
|
||||
const intent = { sessionId: "session-a", token: 3, minItemCount: 12 }
|
||||
|
||||
assert.equal(
|
||||
shouldClearSessionBottomFollowIntent(intent, {
|
||||
sessionId: "session-a",
|
||||
messageCount: 11,
|
||||
streamingActive: false,
|
||||
}),
|
||||
false,
|
||||
)
|
||||
|
||||
assert.equal(
|
||||
shouldClearSessionBottomFollowIntent(intent, {
|
||||
sessionId: "session-a",
|
||||
messageCount: 12,
|
||||
streamingActive: true,
|
||||
}),
|
||||
false,
|
||||
)
|
||||
|
||||
assert.equal(
|
||||
shouldClearSessionBottomFollowIntent(intent, {
|
||||
sessionId: "session-a",
|
||||
messageCount: 12,
|
||||
streamingActive: false,
|
||||
}),
|
||||
true,
|
||||
)
|
||||
|
||||
assert.equal(
|
||||
shouldClearSessionBottomFollowIntent(intent, {
|
||||
sessionId: "session-b",
|
||||
messageCount: 12,
|
||||
streamingActive: false,
|
||||
}),
|
||||
false,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { VirtualFollowBottomIntent } from "../virtual-follow-list"
|
||||
|
||||
export interface SessionBottomFollowIntent extends VirtualFollowBottomIntent {
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
export function resolveSessionBottomFollowIntent(
|
||||
intent: SessionBottomFollowIntent | null,
|
||||
sessionId: string,
|
||||
): VirtualFollowBottomIntent | null {
|
||||
if (!intent || intent.sessionId !== sessionId) return null
|
||||
return { token: intent.token, minItemCount: intent.minItemCount }
|
||||
}
|
||||
|
||||
export function shouldClearSessionBottomFollowIntent(
|
||||
intent: SessionBottomFollowIntent | null,
|
||||
state: { sessionId: string; messageCount: number; streamingActive: boolean },
|
||||
) {
|
||||
if (!intent) return false
|
||||
if (intent.sessionId !== state.sessionId) return false
|
||||
return state.messageCount >= (intent.minItemCount ?? 0) && !state.streamingActive
|
||||
}
|
||||
|
|
@ -20,6 +20,8 @@ import { clearConversationPlaybackForSession } from "../../stores/conversation-s
|
|||
import { useConfig } from "../../stores/preferences"
|
||||
import { closeSessionPreview, getSessionPreview, showSessionChat } from "../../stores/session-previews"
|
||||
import { SessionPreviewView } from "../session-preview-view"
|
||||
import { isSnapshotAutoFollowing } from "../virtual-follow-behavior"
|
||||
import { resolveSessionBottomFollowIntent, shouldClearSessionBottomFollowIntent, type SessionBottomFollowIntent } from "./session-bottom-follow-intent"
|
||||
|
||||
const log = getLogger("session")
|
||||
|
||||
|
|
@ -79,19 +81,23 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
let scrollToBottomHandle: (() => void) | undefined
|
||||
let rootRef: HTMLDivElement | undefined
|
||||
const pendingIdleSeenTimers = new Set<string>()
|
||||
const [pendingSubmitBottomScrollTargetCount, setPendingSubmitBottomScrollTargetCount] = createSignal<number | null>(null)
|
||||
const [submitBottomFollowIntent, setSubmitBottomFollowIntent] = createSignal<SessionBottomFollowIntent | null>(null)
|
||||
let submitBottomFollowIntentSequence = 0
|
||||
|
||||
function shouldScrollToBottomOnActivate() {
|
||||
const current = session()
|
||||
if (!current) return true
|
||||
const snapshot = messageStore().getScrollSnapshot(current.id, MESSAGE_SCROLL_CACHE_SCOPE)
|
||||
return !snapshot || snapshot.atBottom
|
||||
return isSnapshotAutoFollowing(snapshot)
|
||||
}
|
||||
|
||||
function scheduleScrollToBottom(options?: { force?: boolean }) {
|
||||
function scheduleScrollToBottom(options?: { force?: boolean; sessionId?: string }) {
|
||||
if (!scrollToBottomHandle) return false
|
||||
const targetSessionId = options?.sessionId ?? props.sessionId
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const current = session()
|
||||
if (!current || current.id !== targetSessionId) return
|
||||
if (!options?.force && !shouldScrollToBottomOnActivate()) return
|
||||
scrollToBottomHandle?.()
|
||||
})
|
||||
|
|
@ -99,6 +105,33 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
return true
|
||||
}
|
||||
|
||||
function startSubmitBottomFollowIntent(minItemCount: number) {
|
||||
submitBottomFollowIntentSequence += 1
|
||||
setSubmitBottomFollowIntent({ sessionId: props.sessionId, token: submitBottomFollowIntentSequence, minItemCount })
|
||||
}
|
||||
|
||||
function forceSubmittedExchangeToBottom(minItemCount: number) {
|
||||
startSubmitBottomFollowIntent(minItemCount)
|
||||
scrollToBottomHandle?.()
|
||||
}
|
||||
|
||||
const activeSubmitBottomFollowIntent = createMemo(() => {
|
||||
const intent = submitBottomFollowIntent()
|
||||
const currentSession = session()
|
||||
if (!intent || !currentSession) return null
|
||||
|
||||
const messageCount = messageStore().getSessionMessageIds(currentSession.id).length
|
||||
if (shouldClearSessionBottomFollowIntent(intent, {
|
||||
sessionId: currentSession.id,
|
||||
messageCount,
|
||||
streamingActive: sessionStreamingActive(),
|
||||
})) {
|
||||
return null
|
||||
}
|
||||
|
||||
return resolveSessionBottomFollowIntent(intent, currentSession.id)
|
||||
})
|
||||
|
||||
function getSeenIdleEntries(currentSession: Session, keepUnseenSubagentIdleStatus: boolean): Array<{ id: string; idleSince: number }> {
|
||||
const entries: Array<{ id: string; idleSince: number }> = []
|
||||
|
||||
|
|
@ -130,6 +163,21 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const intent = submitBottomFollowIntent()
|
||||
const currentSession = session()
|
||||
if (!intent || !currentSession) return
|
||||
|
||||
const messageCount = messageStore().getSessionMessageIds(currentSession.id).length
|
||||
if (shouldClearSessionBottomFollowIntent(intent, {
|
||||
sessionId: currentSession.id,
|
||||
messageCount,
|
||||
streamingActive: sessionStreamingActive(),
|
||||
})) {
|
||||
setSubmitBottomFollowIntent(null)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const currentSession = session()
|
||||
if (!props.isActive || !currentSession) return
|
||||
|
|
@ -216,21 +264,6 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => messageStore().getSessionMessageIds(props.sessionId).length,
|
||||
(messageCount) => {
|
||||
const targetCount = pendingSubmitBottomScrollTargetCount()
|
||||
if (targetCount === null) return
|
||||
const didSchedule = scheduleScrollToBottom({ force: true })
|
||||
if (didSchedule && messageCount >= targetCount) {
|
||||
setPendingSubmitBottomScrollTargetCount(null)
|
||||
}
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
function registerPromptInputApi(api: PromptInputApi) {
|
||||
promptInputApi = api
|
||||
props.registerSessionPromptApi?.(props.sessionId, api)
|
||||
|
|
@ -276,14 +309,14 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
|
||||
async function handleSendMessage(prompt: string, attachments: Attachment[]) {
|
||||
const messageCount = messageStore().getSessionMessageIds(props.sessionId).length
|
||||
setPendingSubmitBottomScrollTargetCount(messageCount + 2)
|
||||
scheduleScrollToBottom({ force: true })
|
||||
const submittedExchangeTargetCount = messageCount + 2
|
||||
forceSubmittedExchangeToBottom(submittedExchangeTargetCount)
|
||||
try {
|
||||
await sendMessage(props.instanceId, props.sessionId, prompt, attachments)
|
||||
scheduleScrollToBottom({ force: true })
|
||||
setPendingSubmitBottomScrollTargetCount(null)
|
||||
const latestMessageCount = messageStore().getSessionMessageIds(props.sessionId).length
|
||||
forceSubmittedExchangeToBottom(Math.max(submittedExchangeTargetCount, latestMessageCount))
|
||||
} catch (error) {
|
||||
setPendingSubmitBottomScrollTargetCount(null)
|
||||
setSubmitBottomFollowIntent(null)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
@ -448,20 +481,13 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
loadError={messagesLoadError()}
|
||||
onReloadMessages={handleReloadMessages}
|
||||
sessionStreamingActive={sessionStreamingActive()}
|
||||
bottomFollowIntent={activeSubmitBottomFollowIntent()}
|
||||
onRevert={handleRevert}
|
||||
onDeleteMessagesUpTo={handleDeleteMessagesUpTo}
|
||||
onFork={handleFork}
|
||||
isActive={props.isActive}
|
||||
registerScrollToBottom={(fn) => {
|
||||
scrollToBottomHandle = fn ?? undefined
|
||||
if (!fn) return
|
||||
const targetCount = pendingSubmitBottomScrollTargetCount()
|
||||
if (targetCount === null) return
|
||||
const didSchedule = scheduleScrollToBottom({ force: true })
|
||||
const messageCount = messageStore().getSessionMessageIds(props.sessionId).length
|
||||
if (didSchedule && messageCount >= targetCount) {
|
||||
setPendingSubmitBottomScrollTargetCount(null)
|
||||
}
|
||||
}}
|
||||
showSidebarToggle={props.showSidebarToggle}
|
||||
onSidebarToggle={props.onSidebarToggle}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,14 @@ import { createEffect, createMemo, createSignal, For, type Component } from "sol
|
|||
import { Check, ChevronDown, Laptop, Moon, Sun } from "lucide-solid"
|
||||
import { useI18n } from "../../lib/i18n"
|
||||
import { useTheme, type ThemeMode } from "../../lib/theme"
|
||||
import { useConfig } from "../../stores/preferences"
|
||||
import { useConfig, type ExpansionPreference, type ToolCallExpansionPreset } from "../../stores/preferences"
|
||||
import { getBehaviorSettings, type BehaviorSetting } from "../../lib/settings/behavior-registry"
|
||||
import {
|
||||
buildToolExpansionPresetDefaults,
|
||||
getConfigurableToolEntries,
|
||||
OTHER_TOOL_NAME,
|
||||
THINKING_EXPANSION_PRESETS,
|
||||
} from "../tool-call/tool-registry"
|
||||
|
||||
const themeModeOptions: Array<{ value: ThemeMode; icon: typeof Laptop }> = [
|
||||
{ value: "system", icon: Laptop },
|
||||
|
|
@ -12,6 +18,8 @@ const themeModeOptions: Array<{ value: ThemeMode; icon: typeof Laptop }> = [
|
|||
{ value: "dark", icon: Moon },
|
||||
]
|
||||
|
||||
const toolExpansionPresetOptions: ToolCallExpansionPreset[] = ["minimal", "balanced", "detailed", "everything"]
|
||||
|
||||
export const AppearanceSettingsSection: Component = () => {
|
||||
const { t } = useI18n()
|
||||
const { themeMode, setThemeMode } = useTheme()
|
||||
|
|
@ -45,16 +53,18 @@ export const AppearanceSettingsSection: Component = () => {
|
|||
toggleKeyboardShortcutHints,
|
||||
toggleShowMessageTimeline,
|
||||
toggleShowTimelineTools,
|
||||
toggleUsageMetrics,
|
||||
toggleAutoCleanupBlankSessions,
|
||||
togglePromptSubmitOnEnter,
|
||||
toggleShowPromptVoiceInput,
|
||||
setDiffViewMode,
|
||||
toggleUsageMetrics,
|
||||
toggleAutoCleanupBlankSessions,
|
||||
togglePromptSubmitOnEnter,
|
||||
toggleShowPromptVoiceInput,
|
||||
setDiffViewMode,
|
||||
setToolOutputExpansion,
|
||||
setDiagnosticsExpansion,
|
||||
setThinkingBlocksExpansion,
|
||||
setToolInputsVisibility,
|
||||
}),
|
||||
}).filter(
|
||||
(setting) => setting.id !== "behavior.thinkingBlocksDefault" && setting.id !== "behavior.toolOutputsDefault",
|
||||
),
|
||||
)
|
||||
|
||||
const [overrides, setOverrides] = createSignal<Map<string, unknown>>(new Map())
|
||||
|
|
@ -99,6 +109,78 @@ export const AppearanceSettingsSection: Component = () => {
|
|||
|
||||
type SelectOption = { value: string; label: string }
|
||||
|
||||
type ExpansionRow =
|
||||
| { kind: "thinking"; key: "thinking"; label: string }
|
||||
| { kind: "tool"; key: string; label: string }
|
||||
|
||||
const expansionOptions = createMemo<SelectOption[]>(() => [
|
||||
{ value: "collapsed", label: t("commands.common.collapsed") },
|
||||
{ value: "expanded", label: t("commands.common.expanded") },
|
||||
])
|
||||
|
||||
const toolExpansionRows = createMemo<ExpansionRow[]>(() => [
|
||||
{ kind: "thinking", key: "thinking", label: t("settings.behavior.expansionDefaults.thinking") },
|
||||
...getConfigurableToolEntries().map((entry) => ({
|
||||
kind: "tool" as const,
|
||||
key: entry.tool,
|
||||
label: entry.labelKey ? t(entry.labelKey) : entry.label,
|
||||
})),
|
||||
])
|
||||
|
||||
const currentPreset = createMemo(() => preferences().toolCallExpansionDefaults.preset)
|
||||
|
||||
const currentToolMode = (tool: string): ExpansionPreference => {
|
||||
const pref = preferences().toolCallExpansionDefaults
|
||||
const entry = getConfigurableToolEntries().find((item) => item.tool === tool)
|
||||
if (pref.tools[tool]) return pref.tools[tool]
|
||||
if (pref.preset !== "custom" && entry) return entry.expansionPresets[pref.preset]
|
||||
return pref.tools[OTHER_TOOL_NAME] ?? "expanded"
|
||||
}
|
||||
|
||||
const currentThinkingMode = (): ExpansionPreference => {
|
||||
const pref = preferences().toolCallExpansionDefaults
|
||||
if (pref.thinking) return pref.thinking
|
||||
if (pref.preset !== "custom") return THINKING_EXPANSION_PRESETS[pref.preset]
|
||||
return preferences().thinkingBlocksExpansion ?? "expanded"
|
||||
}
|
||||
|
||||
const materializeToolModes = () => {
|
||||
const tools: Record<string, ExpansionPreference> = {}
|
||||
for (const entry of getConfigurableToolEntries()) {
|
||||
tools[entry.tool] = currentToolMode(entry.tool)
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
const applyExpansionPreset = (preset: ToolCallExpansionPreset) => {
|
||||
const tools = buildToolExpansionPresetDefaults(preset)
|
||||
const thinking = THINKING_EXPANSION_PRESETS[preset]
|
||||
updatePreferences({
|
||||
toolCallExpansionDefaults: { preset, thinking, tools },
|
||||
thinkingBlocksExpansion: thinking,
|
||||
toolOutputExpansion: tools[OTHER_TOOL_NAME] ?? "expanded",
|
||||
})
|
||||
}
|
||||
|
||||
const setExpansionRowMode = (row: ExpansionRow, mode: ExpansionPreference) => {
|
||||
const tools = materializeToolModes()
|
||||
const thinking = row.kind === "thinking" ? mode : currentThinkingMode()
|
||||
if (row.kind === "tool") {
|
||||
tools[row.key] = mode
|
||||
}
|
||||
updatePreferences({
|
||||
toolCallExpansionDefaults: { preset: "custom", thinking, tools },
|
||||
thinkingBlocksExpansion: thinking,
|
||||
toolOutputExpansion: tools[OTHER_TOOL_NAME] ?? preferences().toolOutputExpansion,
|
||||
})
|
||||
}
|
||||
|
||||
const rowMode = (row: ExpansionRow): ExpansionPreference =>
|
||||
row.kind === "thinking" ? currentThinkingMode() : currentToolMode(row.key)
|
||||
|
||||
const selectedExpansionOption = (mode: ExpansionPreference) =>
|
||||
expansionOptions().find((opt) => opt.value === mode)
|
||||
|
||||
const BehaviorRow: Component<{ setting: BehaviorSetting }> = (props) => {
|
||||
const setting = props.setting
|
||||
const disabled = createMemo(() => (setting.disabled ? Boolean(setting.disabled()) : false))
|
||||
|
|
@ -224,6 +306,8 @@ export const AppearanceSettingsSection: Component = () => {
|
|||
return t("theme.mode.dark")
|
||||
}
|
||||
|
||||
const presetLabel = (preset: ToolCallExpansionPreset | "custom") => t(`settings.behavior.expansionPreset.${preset}.title`)
|
||||
|
||||
return (
|
||||
<div class="settings-section-stack">
|
||||
<div class="settings-card">
|
||||
|
|
@ -266,7 +350,80 @@ export const AppearanceSettingsSection: Component = () => {
|
|||
<h3 class="settings-card-title">{t("settings.appearance.behavior.title")}</h3>
|
||||
<p class="settings-card-subtitle">{t("settings.appearance.behavior.subtitle")}</p>
|
||||
</div>
|
||||
<span class="settings-scope-badge">{t("settings.scope.device")}</span>
|
||||
<span class="settings-scope-badge">{presetLabel(currentPreset())}</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-expansion-presets" aria-label={t("settings.behavior.expansionPresets.ariaLabel")}>
|
||||
<For each={toolExpansionPresetOptions}>
|
||||
{(preset) => (
|
||||
<button
|
||||
type="button"
|
||||
class="settings-expansion-preset"
|
||||
data-selected={currentPreset() === preset ? "true" : "false"}
|
||||
onClick={() => applyExpansionPreset(preset)}
|
||||
>
|
||||
<span class="settings-expansion-preset-title">{presetLabel(preset)}</span>
|
||||
<span class="settings-expansion-preset-copy">{t(`settings.behavior.expansionPreset.${preset}.description`)}</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="settings-expansion-table" role="table" aria-label={t("settings.behavior.expansionDefaults.title")}>
|
||||
<div class="settings-expansion-table-header" role="row">
|
||||
<span role="columnheader">{t("settings.behavior.expansionDefaults.itemColumn")}</span>
|
||||
<span role="columnheader">{t("settings.behavior.expansionDefaults.stateColumn")}</span>
|
||||
</div>
|
||||
<For each={toolExpansionRows()}>
|
||||
{(row) => {
|
||||
const selected = createMemo(() => selectedExpansionOption(rowMode(row)))
|
||||
return (
|
||||
<div class="settings-expansion-row" role="row">
|
||||
<div class="settings-expansion-row-label" role="cell">
|
||||
<code>{row.label}</code>
|
||||
</div>
|
||||
<div class="settings-expansion-row-control" role="cell">
|
||||
<Select<SelectOption>
|
||||
value={selected()}
|
||||
onChange={(opt) => {
|
||||
if (!opt) return
|
||||
setExpansionRowMode(row, opt.value as ExpansionPreference)
|
||||
}}
|
||||
options={expansionOptions()}
|
||||
optionValue="value"
|
||||
optionTextValue="label"
|
||||
itemComponent={(itemProps) => (
|
||||
<Select.Item item={itemProps.item} class="selector-option">
|
||||
<Select.ItemLabel class="selector-option-label">{itemProps.item.rawValue.label}</Select.ItemLabel>
|
||||
</Select.Item>
|
||||
)}
|
||||
>
|
||||
<Select.Trigger class="selector-trigger settings-expansion-select" aria-label={t("settings.behavior.expansionDefaults.rowAriaLabel", { item: row.label })}>
|
||||
<div class="flex-1 min-w-0">
|
||||
<Select.Value<SelectOption>>
|
||||
{(state) => (
|
||||
<span class="selector-trigger-primary selector-trigger-primary--align-left">
|
||||
{state.selectedOption()?.label}
|
||||
</span>
|
||||
)}
|
||||
</Select.Value>
|
||||
</div>
|
||||
<Select.Icon class="selector-trigger-icon">
|
||||
<ChevronDown class="w-3 h-3" />
|
||||
</Select.Icon>
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Portal>
|
||||
<Select.Content class="selector-popover">
|
||||
<Select.Listbox class="selector-listbox" />
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="settings-stack">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createSignal, Show, createEffect, createMemo, onCleanup, type Accessor } from "solid-js"
|
||||
import { ArrowRightSquare, Check, Copy, Hourglass, Loader2, Volume2, XCircle } from "lucide-solid"
|
||||
import { createSignal, Show, createEffect, createMemo, onCleanup, type Accessor, type JSXElement } from "solid-js"
|
||||
import { ArrowRightSquare, Check, Copy, Hourglass, Loader2, Volume2, WrapText, XCircle } from "lucide-solid"
|
||||
import { stringify as stringifyYaml } from "yaml"
|
||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||
import { useTheme } from "../lib/theme"
|
||||
|
|
@ -12,6 +12,7 @@ import { getPermissionSessionId } from "../types/permission"
|
|||
import type { QuestionRequest } from "../types/question"
|
||||
import { useI18n } from "../lib/i18n"
|
||||
import { resolveToolRenderer } from "./tool-call/renderers"
|
||||
import { resolveToolExpansionDefault } from "./tool-call/tool-registry"
|
||||
import { QuestionToolBlock } from "./tool-call/question-block"
|
||||
import { PermissionToolBlock } from "./tool-call/permission-block"
|
||||
import { createAnsiContentRenderer } from "./tool-call/ansi-render"
|
||||
|
|
@ -25,6 +26,7 @@ import type {
|
|||
MarkdownRenderOptions,
|
||||
AnsiRenderOptions,
|
||||
ToolCallPart,
|
||||
ToolOutputChrome,
|
||||
ToolRendererContext,
|
||||
ToolScrollHelpers,
|
||||
} from "./tool-call/types"
|
||||
|
|
@ -32,7 +34,6 @@ import {
|
|||
buildToolSpeechText,
|
||||
ensureMarkdownContent,
|
||||
getRelativePath,
|
||||
getToolIcon,
|
||||
getToolName,
|
||||
isToolStateCompleted,
|
||||
isToolStateError,
|
||||
|
|
@ -40,12 +41,11 @@ import {
|
|||
getDefaultToolAction,
|
||||
readToolStatePayload,
|
||||
} from "./tool-call/utils"
|
||||
import { resolveTitleForTool } from "./tool-call/tool-title"
|
||||
import { getLogger } from "../lib/logger"
|
||||
import { useSpeech } from "../lib/hooks/use-speech"
|
||||
import SpeechActionButton from "./speech-action-button"
|
||||
import { createFollowScroll } from "../lib/follow-scroll"
|
||||
import ActionOverflowMenu, { type ActionOverflowMenuItem } from "./action-overflow-menu"
|
||||
import SpeechActionButton from "./speech-action-button"
|
||||
|
||||
const log = getLogger("session")
|
||||
|
||||
|
|
@ -80,6 +80,8 @@ interface ToolCallProps {
|
|||
* Users can still expand/collapse manually.
|
||||
*/
|
||||
forceCollapsed?: boolean
|
||||
headerAction?: JSXElement
|
||||
headerMenuItems?: () => ActionOverflowMenuItem[]
|
||||
}
|
||||
|
||||
function ToolStatusIndicator(props: { status: Accessor<string> }) {
|
||||
|
|
@ -128,6 +130,8 @@ function ToolCallDetails(props: {
|
|||
toolInput: () => Record<string, any> | undefined
|
||||
inputSectionExpanded: () => boolean
|
||||
outputSectionExpanded: () => boolean
|
||||
outputWrapEnabled: Accessor<boolean>
|
||||
toggleOutputWrap: () => void
|
||||
toggleInputSection: () => void
|
||||
toggleOutputSection: () => void
|
||||
toolCallRootEl: () => HTMLDivElement | undefined
|
||||
|
|
@ -186,7 +190,6 @@ function ToolCallDetails(props: {
|
|||
|
||||
const [permissionSubmitting, setPermissionSubmitting] = createSignal(false)
|
||||
const [permissionError, setPermissionError] = createSignal<string | null>(null)
|
||||
const [permissionRejectReasonOpen, setPermissionRejectReasonOpen] = createSignal(false)
|
||||
|
||||
const followScroll = createFollowScroll({
|
||||
getScrollTopSnapshot: props.scrollTopSnapshot,
|
||||
|
|
@ -247,7 +250,6 @@ function ToolCallDetails(props: {
|
|||
if (!activeKey) return
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (isTextInputFocused()) return
|
||||
if (permissionRejectReasonOpen()) return
|
||||
const permission = permissionDetails()
|
||||
if (!permission || !props.isPermissionActive()) return
|
||||
if (event.key === "Enter") {
|
||||
|
|
@ -354,18 +356,20 @@ function ToolCallDetails(props: {
|
|||
|
||||
const status = () => props.toolState()?.status || ""
|
||||
|
||||
const toolInputMarkdown = createMemo(() => {
|
||||
const toolInputDisplay = createMemo((): { content: string; copyText: string; language: string } | null => {
|
||||
const input = props.toolInput()
|
||||
if (!input || Object.keys(input).length === 0) return null
|
||||
|
||||
try {
|
||||
const yamlText = stringifyYaml(input)
|
||||
return ensureMarkdownContent(yamlText, "yaml", true)
|
||||
const content = ensureMarkdownContent(yamlText, "yaml", true)
|
||||
return content ? { content, copyText: yamlText, language: "yaml" } : null
|
||||
} catch (error) {
|
||||
log.error("Failed to convert tool call input to YAML", error)
|
||||
try {
|
||||
const jsonText = JSON.stringify(input, null, 2)
|
||||
return ensureMarkdownContent(jsonText, "json", true)
|
||||
const content = ensureMarkdownContent(jsonText, "json", true)
|
||||
return content ? { content, copyText: jsonText, language: "json" } : null
|
||||
} catch (nestedError) {
|
||||
log.error("Failed to stringify tool call input", nestedError)
|
||||
return null
|
||||
|
|
@ -407,6 +411,9 @@ function ToolCallDetails(props: {
|
|||
onContentRendered: props.onContentRendered,
|
||||
})
|
||||
|
||||
const renderOutputMarkdownContent: ToolRendererContext["renderMarkdown"] = (options) =>
|
||||
renderMarkdownContent({ ...options, wrap: options.wrap ?? props.outputWrapEnabled() })
|
||||
|
||||
const rendererContext: ToolRendererContext = {
|
||||
toolCall: props.toolCallMemo,
|
||||
toolState: props.toolState,
|
||||
|
|
@ -416,7 +423,7 @@ function ToolCallDetails(props: {
|
|||
t: props.t,
|
||||
messageVersion: messageVersionAccessor,
|
||||
partVersion: partVersionAccessor,
|
||||
renderMarkdown: renderMarkdownContent,
|
||||
renderMarkdown: renderOutputMarkdownContent,
|
||||
renderAnsi: renderAnsiContent,
|
||||
renderDiff: renderDiffContent,
|
||||
renderToolCall: (options) => {
|
||||
|
|
@ -435,6 +442,7 @@ function ToolCallDetails(props: {
|
|||
/>
|
||||
)
|
||||
},
|
||||
outputWrapEnabled: props.outputWrapEnabled,
|
||||
scrollHelpers,
|
||||
onContentRendered: props.onContentRendered,
|
||||
}
|
||||
|
|
@ -462,6 +470,8 @@ function ToolCallDetails(props: {
|
|||
return renderer().renderBody(rendererContext)
|
||||
}
|
||||
|
||||
const outputChrome = createMemo<ToolOutputChrome>(() => renderer().getOutputChrome?.(rendererContext) ?? {})
|
||||
|
||||
const renderError = () => {
|
||||
const state = props.toolState()
|
||||
if (state?.status === "error" && state.error) {
|
||||
|
|
@ -482,7 +492,6 @@ function ToolCallDetails(props: {
|
|||
error={permissionError}
|
||||
renderDiff={renderDiffContent}
|
||||
fallbackSessionId={() => props.sessionId}
|
||||
onRejectReasonOpenChange={setPermissionRejectReasonOpen}
|
||||
onRespond={(permission, sessionId, response, message) => void handlePermissionResponse(permission, response, message)}
|
||||
/>
|
||||
)
|
||||
|
|
@ -503,59 +512,190 @@ function ToolCallDetails(props: {
|
|||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div class="tool-call-details">
|
||||
<Show
|
||||
when={props.isToolInputVisible() && props.hasToolInput()}
|
||||
fallback={
|
||||
<>
|
||||
{renderToolBody()}
|
||||
{renderError()}
|
||||
const shouldShowPendingMessage = () => {
|
||||
const tool = props.toolName()
|
||||
return status() === "pending" && !props.pendingPermission() && tool !== "todowrite"
|
||||
}
|
||||
|
||||
<Show when={status() === "pending" && !props.pendingPermission()}>
|
||||
const copyIoText = async (event: MouseEvent, text?: string | null) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!text) return
|
||||
await copyToClipboard(text)
|
||||
}
|
||||
|
||||
const outputWrapTitle = () =>
|
||||
props.outputWrapEnabled()
|
||||
? props.t("toolCall.diff.disableWordWrap")
|
||||
: props.t("toolCall.diff.enableWordWrap")
|
||||
|
||||
const renderIoHeader = (options: {
|
||||
title: () => string
|
||||
language?: () => string | null | undefined
|
||||
expanded: () => boolean
|
||||
onToggle: () => void
|
||||
copyText?: () => string | null | undefined
|
||||
copyTitle?: () => string
|
||||
copyAriaLabel?: () => string
|
||||
actions?: () => JSXElement
|
||||
wrapToggle?: () => boolean | undefined
|
||||
}) => (
|
||||
<div class="tool-call-io-header">
|
||||
<button type="button" class="tool-call-io-toggle" aria-expanded={options.expanded()} onClick={options.onToggle}>
|
||||
<span class="tool-call-io-disclosure" aria-hidden="true">{options.expanded() ? "▼" : "▶"}</span>
|
||||
<span class="tool-call-io-title">{options.title()}</span>
|
||||
<Show when={options.language?.()}>
|
||||
{(language) => <span class="tool-call-io-language">{language()}</span>}
|
||||
</Show>
|
||||
</button>
|
||||
|
||||
<Show when={options.actions?.()}>
|
||||
{(actions) => <span class="tool-call-io-actions">{actions()}</span>}
|
||||
</Show>
|
||||
|
||||
<Show when={options.copyText?.()}>
|
||||
{(copyText) => (
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-header-icon-button tool-call-header-copy tool-call-io-copy"
|
||||
onClick={(event) => void copyIoText(event, copyText())}
|
||||
aria-label={options.copyAriaLabel?.() ?? props.t("toolCall.io.copyOutputAriaLabel")}
|
||||
title={options.copyTitle?.() ?? props.t("toolCall.io.copyOutputTitle")}
|
||||
>
|
||||
<Copy class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={options.wrapToggle?.()}>
|
||||
<button
|
||||
type="button"
|
||||
class={`tool-call-header-icon-button tool-call-header-copy tool-call-io-wrap${props.outputWrapEnabled() ? " active" : ""}`}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
props.toggleOutputWrap()
|
||||
}}
|
||||
aria-label={outputWrapTitle()}
|
||||
title={outputWrapTitle()}
|
||||
>
|
||||
<WrapText class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
||||
const shouldUseToolHeaderOutputControls = () => Boolean(outputChrome().wrapToggle)
|
||||
|
||||
const renderToolOutputBody = () => {
|
||||
const body = renderToolBody()
|
||||
const error = renderError()
|
||||
const showPending = shouldShowPendingMessage()
|
||||
const chrome = outputChrome()
|
||||
|
||||
if (!body && !error && !showPending) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (chrome.wrapToggle) {
|
||||
return (
|
||||
<div class="tool-call-body">
|
||||
<div class="tool-call-io-body" data-suppress-inner-header={chrome.suppressInnerHeader === false ? undefined : "true"}>
|
||||
{body}
|
||||
{error}
|
||||
|
||||
<Show when={showPending}>
|
||||
<div class="tool-call-pending-message">
|
||||
<span class="spinner-small"></span>
|
||||
<span>{props.t("toolCall.pending.waitingToRun")}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div class="tool-call-io-sections">
|
||||
<div class="tool-call-io-section">
|
||||
<button type="button" class="tool-call-io-toggle" aria-expanded={props.inputSectionExpanded()} onClick={props.toggleInputSection}>
|
||||
<span class="tool-call-io-title">{props.t("toolCall.io.input")}</span>
|
||||
</button>
|
||||
|
||||
<Show when={props.inputSectionExpanded()}>
|
||||
<div class="tool-call-io-body">
|
||||
{(() => {
|
||||
const content = toolInputMarkdown()
|
||||
if (!content) return null
|
||||
return renderMarkdownContent({ content, cacheKey: "input" })
|
||||
})()}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="tool-call-io-section">
|
||||
<button type="button" class="tool-call-io-toggle" aria-expanded={props.outputSectionExpanded()} onClick={props.toggleOutputSection}>
|
||||
<span class="tool-call-io-title">{props.t("toolCall.io.output")}</span>
|
||||
</button>
|
||||
return (
|
||||
<div class="tool-call-body">
|
||||
{body}
|
||||
{error}
|
||||
|
||||
<Show when={props.outputSectionExpanded()}>
|
||||
<div class="tool-call-io-body">
|
||||
{renderToolBody()}
|
||||
{renderError()}
|
||||
<Show when={showPending}>
|
||||
<div class="tool-call-pending-message">
|
||||
<span class="spinner-small"></span>
|
||||
<span>{props.t("toolCall.pending.waitingToRun")}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<Show when={status() === "pending" && !props.pendingPermission()}>
|
||||
<div class="tool-call-pending-message">
|
||||
<span class="spinner-small"></span>
|
||||
<span>{props.t("toolCall.pending.waitingToRun")}</span>
|
||||
return (
|
||||
<div class="tool-call-details">
|
||||
<Show
|
||||
when={props.isToolInputVisible() && props.hasToolInput()}
|
||||
fallback={renderToolOutputBody()}
|
||||
>
|
||||
<div class="tool-call-body">
|
||||
<div class="tool-call-io-sections">
|
||||
<div class="tool-call-io-section">
|
||||
{(() => {
|
||||
return renderIoHeader({
|
||||
title: () => props.t("toolCall.io.input"),
|
||||
language: () => toolInputDisplay()?.language,
|
||||
expanded: props.inputSectionExpanded,
|
||||
onToggle: props.toggleInputSection,
|
||||
copyText: () => toolInputDisplay()?.copyText,
|
||||
copyTitle: () => props.t("toolCall.io.copyInputTitle"),
|
||||
copyAriaLabel: () => props.t("toolCall.io.copyInputAriaLabel"),
|
||||
})
|
||||
})()}
|
||||
|
||||
<Show when={props.inputSectionExpanded()}>
|
||||
<div class="tool-call-io-body" data-suppress-inner-header="true">
|
||||
{(() => {
|
||||
const input = toolInputDisplay()
|
||||
if (!input) return null
|
||||
return renderMarkdownContent({ content: input.content, cacheKey: "input" })
|
||||
})()}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="tool-call-io-section">
|
||||
<Show when={!shouldUseToolHeaderOutputControls()}>
|
||||
{renderIoHeader({
|
||||
title: () => outputChrome().title || props.t("toolCall.io.output"),
|
||||
language: () => outputChrome().language,
|
||||
expanded: props.outputSectionExpanded,
|
||||
onToggle: props.toggleOutputSection,
|
||||
copyText: () => outputChrome().copyText,
|
||||
copyTitle: () => props.t("toolCall.io.copyOutputTitle"),
|
||||
copyAriaLabel: () => props.t("toolCall.io.copyOutputAriaLabel"),
|
||||
actions: () => outputChrome().actions,
|
||||
wrapToggle: () => outputChrome().wrapToggle,
|
||||
})}
|
||||
</Show>
|
||||
|
||||
<Show when={shouldUseToolHeaderOutputControls() || props.outputSectionExpanded()}>
|
||||
<div>
|
||||
<div
|
||||
class="tool-call-io-body"
|
||||
data-suppress-inner-header={outputChrome().suppressInnerHeader === false ? undefined : "true"}
|
||||
>
|
||||
{renderToolBody()}
|
||||
{renderError()}
|
||||
|
||||
<Show when={shouldShowPendingMessage()}>
|
||||
<div class="tool-call-pending-message">
|
||||
<span class="spinner-small"></span>
|
||||
<span>{props.t("toolCall.pending.waitingToRun")}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -606,26 +746,21 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
return undefined
|
||||
})
|
||||
|
||||
const toolOutputDefaultExpanded = createMemo(() => (preferences().toolOutputExpansion || "expanded") === "expanded")
|
||||
const diagnosticsDefaultExpanded = createMemo(() => (preferences().diagnosticsExpansion || "expanded") === "expanded")
|
||||
|
||||
const defaultExpandedForTool = createMemo(() => {
|
||||
if (props.forceCollapsed) {
|
||||
return false
|
||||
}
|
||||
const prefExpanded = toolOutputDefaultExpanded()
|
||||
const toolName = toolCallMemo()?.tool || ""
|
||||
if (toolName === "read") {
|
||||
const state = toolState()
|
||||
if (state?.status === "error") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
const state = toolState()
|
||||
if (state?.status === "error") {
|
||||
return true
|
||||
}
|
||||
return prefExpanded
|
||||
return resolveToolExpansionDefault(preferences(), toolCallMemo()?.tool || "")
|
||||
})
|
||||
|
||||
const [userExpanded, setUserExpanded] = createSignal<boolean | null>(null)
|
||||
const [outputWrapEnabled, setOutputWrapEnabled] = createSignal(true)
|
||||
const toolInputsVisibility = createMemo(() => preferences().toolInputsVisibility || "collapsed")
|
||||
const [toolInputVisibilityOverride, setToolInputVisibilityOverride] = createSignal<"hidden" | "expanded" | null>(null)
|
||||
const effectiveToolInputsVisibility = createMemo(() => toolInputVisibilityOverride() ?? toolInputsVisibility())
|
||||
|
|
@ -706,6 +841,9 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
return !current
|
||||
})
|
||||
}
|
||||
|
||||
const toggleOutputWrap = () => setOutputWrapEnabled((enabled) => !enabled)
|
||||
|
||||
const statusClass = () => {
|
||||
const status = toolState()?.status || "pending"
|
||||
return `tool-call-status-${status}`
|
||||
|
|
@ -735,17 +873,6 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
setOutputSectionOverride(null)
|
||||
})
|
||||
|
||||
const handleToggleInputVisibility = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!expanded()) {
|
||||
toggle()
|
||||
}
|
||||
|
||||
const currentlyVisible = isToolInputVisible()
|
||||
setToolInputVisibilityOverride(currentlyVisible ? "hidden" : "expanded")
|
||||
}
|
||||
|
||||
const renderer = createMemo(() => resolveToolRenderer(toolName()))
|
||||
|
||||
const renderMarkdownStub: ToolRendererContext["renderMarkdown"] = () => null
|
||||
|
|
@ -765,10 +892,12 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
renderAnsi: renderAnsiStub,
|
||||
renderDiff: renderDiffStub,
|
||||
renderToolCall: renderToolCallStub,
|
||||
outputWrapEnabled,
|
||||
scrollHelpers: undefined,
|
||||
}
|
||||
|
||||
const getRendererAction = () => renderer().getAction?.(headerRendererContext) ?? getDefaultToolAction(toolName())
|
||||
const headerOutputChrome = createMemo(() => renderer().getOutputChrome?.(headerRendererContext) ?? {})
|
||||
|
||||
|
||||
const renderToolTitle = () => {
|
||||
|
|
@ -776,7 +905,17 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
const currentTool = toolName()
|
||||
|
||||
if (currentTool !== "task") {
|
||||
return resolveTitleForTool({ toolName: currentTool, state })
|
||||
if (!state || state.status === "pending") return getRendererAction()
|
||||
|
||||
const stateTitle = typeof (state as { title?: string }).title === "string" ? (state as { title?: string }).title : undefined
|
||||
if (stateTitle && stateTitle.length > 0) {
|
||||
return stateTitle
|
||||
}
|
||||
|
||||
const customTitle = renderer().getTitle?.(headerRendererContext)
|
||||
if (customTitle) return customTitle
|
||||
|
||||
return getToolName(currentTool)
|
||||
}
|
||||
|
||||
if (!state) return getRendererAction()
|
||||
|
|
@ -796,11 +935,37 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
return getToolName(currentTool)
|
||||
}
|
||||
|
||||
const toolTypeLabel = createMemo(() => toolName())
|
||||
|
||||
const headerTitleDetail = createMemo(() => {
|
||||
const rawTitle = renderToolTitle().trim()
|
||||
const typeLabel = toolTypeLabel().trim()
|
||||
if (!rawTitle) return ""
|
||||
const labels = [typeLabel, getToolName(toolName()).trim()].filter(Boolean)
|
||||
for (const label of labels) {
|
||||
if (rawTitle === label) return ""
|
||||
if (rawTitle.startsWith(`${label} `)) return rawTitle.slice(label.length).trimStart()
|
||||
if (rawTitle.startsWith(`${label}[`)) return rawTitle.slice(label.length).trimStart()
|
||||
if (rawTitle.startsWith(`${label} · `)) return rawTitle.slice(label.length + 3).trimStart()
|
||||
}
|
||||
return rawTitle
|
||||
})
|
||||
|
||||
const headerText = createMemo(() => {
|
||||
// Keep this as a memo so copy always matches what's rendered.
|
||||
return renderToolTitle()
|
||||
const typeLabel = toolTypeLabel()
|
||||
const detail = headerTitleDetail()
|
||||
return [typeLabel, detail].filter(Boolean).join(" ")
|
||||
})
|
||||
|
||||
const headerCopyText = createMemo(() => headerOutputChrome().copyText || "")
|
||||
const canCopyHeaderOutput = () => headerCopyText().length > 0
|
||||
const canToggleOutputWrap = () => Boolean(headerOutputChrome().wrapToggle)
|
||||
const outputWrapTitle = () =>
|
||||
outputWrapEnabled()
|
||||
? t("toolCall.diff.disableWordWrap")
|
||||
: t("toolCall.diff.enableWordWrap")
|
||||
|
||||
const speechText = createMemo(() =>
|
||||
buildToolSpeechText({
|
||||
title: headerText(),
|
||||
|
|
@ -819,12 +984,12 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
const handleCopyHeader = async (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const text = headerText()
|
||||
const text = headerCopyText()
|
||||
if (!text) return
|
||||
await copyToClipboard(text)
|
||||
}
|
||||
|
||||
const actionMenuItems = (): ActionOverflowMenuItem[] => {
|
||||
const actionMenuItems = (includePrimaryActions = false): ActionOverflowMenuItem[] => {
|
||||
const items: ActionOverflowMenuItem[] = []
|
||||
|
||||
if (hasToolInput()) {
|
||||
|
|
@ -840,26 +1005,43 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
})
|
||||
}
|
||||
|
||||
items.push({
|
||||
key: "copy",
|
||||
label: t("toolCall.header.copyTitle"),
|
||||
icon: <Copy class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: async () => {
|
||||
const text = headerText()
|
||||
if (!text) return
|
||||
await copyToClipboard(text)
|
||||
},
|
||||
})
|
||||
if (includePrimaryActions) {
|
||||
if (canCopyHeaderOutput()) {
|
||||
items.push({
|
||||
key: "copy",
|
||||
label: t("toolCall.header.copyOutputTitle"),
|
||||
icon: <Copy class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: async () => {
|
||||
const text = headerCopyText()
|
||||
if (!text) return
|
||||
await copyToClipboard(text)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (canSpeakToolCall()) {
|
||||
items.push({
|
||||
key: "speak",
|
||||
label: speech.buttonTitle(),
|
||||
icon: <Volume2 class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => void speech.toggle(),
|
||||
})
|
||||
if (canToggleOutputWrap()) {
|
||||
items.push({
|
||||
key: "toggle-output-wrap",
|
||||
label: outputWrapTitle(),
|
||||
icon: <WrapText class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => {
|
||||
toggleOutputWrap()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (canSpeakToolCall()) {
|
||||
items.push({
|
||||
key: "speak",
|
||||
label: speech.buttonTitle(),
|
||||
icon: <Volume2 class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => void speech.toggle(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
items.push(...(props.headerMenuItems?.() ?? []))
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
|
|
@ -879,48 +1061,54 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
data-message-id={props.messageId}
|
||||
data-part-id={toolCallIdentifier()}
|
||||
>
|
||||
<div class="tool-call-header" data-action-overflow={actionMenuItems().length > 1 ? "true" : undefined}>
|
||||
<div class="tool-call-header" data-action-overflow={actionMenuItems(true).length > 0 ? "true" : undefined}>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-header-toggle"
|
||||
onClick={toggle}
|
||||
aria-expanded={expanded()}
|
||||
>
|
||||
<span class="tool-call-summary" data-tool-icon={getToolIcon(toolName())}>
|
||||
{headerText()}
|
||||
<span class="tool-call-disclosure" aria-hidden="true">{expanded() ? "▼" : "▶"}</span>
|
||||
<span class="tool-call-summary">
|
||||
<span class="tool-call-summary-type">{toolTypeLabel()}</span>
|
||||
<Show when={headerTitleDetail()}>
|
||||
{(detail) => <span class="tool-call-summary-title">{detail()}</span>}
|
||||
</Show>
|
||||
<ToolStatusIndicator status={status} />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<Show when={hasToolInput()}>
|
||||
<Show when={canCopyHeaderOutput()}>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-header-input"
|
||||
onClick={handleToggleInputVisibility}
|
||||
aria-pressed={isToolInputVisible()}
|
||||
aria-label={
|
||||
isToolInputVisible()
|
||||
? t("toolCall.header.hideInputAriaLabel")
|
||||
: t("toolCall.header.showInputAriaLabel")
|
||||
}
|
||||
title={isToolInputVisible() ? t("toolCall.header.hideInputTitle") : t("toolCall.header.showInputTitle")}
|
||||
class="tool-call-header-icon-button tool-call-header-copy"
|
||||
onClick={handleCopyHeader}
|
||||
aria-label={t("toolCall.header.copyOutputAriaLabel")}
|
||||
title={t("toolCall.header.copyOutputTitle")}
|
||||
>
|
||||
<ArrowRightSquare class="w-3.5 h-3.5" />
|
||||
<Copy class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-header-copy"
|
||||
onClick={handleCopyHeader}
|
||||
aria-label={t("toolCall.header.copyAriaLabel")}
|
||||
title={t("toolCall.header.copyTitle")}
|
||||
>
|
||||
<Copy class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<Show when={canToggleOutputWrap()}>
|
||||
<button
|
||||
type="button"
|
||||
class={`tool-call-header-icon-button tool-call-header-copy tool-call-header-wrap${outputWrapEnabled() ? " active" : ""}`}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
toggleOutputWrap()
|
||||
}}
|
||||
aria-label={outputWrapTitle()}
|
||||
title={outputWrapTitle()}
|
||||
>
|
||||
<WrapText class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
<Show when={canSpeakToolCall()}>
|
||||
<SpeechActionButton
|
||||
class="tool-call-header-copy"
|
||||
class="tool-call-header-icon-button tool-call-header-copy"
|
||||
onClick={() => void speech.toggle()}
|
||||
title={speech.buttonTitle()}
|
||||
isLoading={speech.isLoading()}
|
||||
|
|
@ -928,14 +1116,22 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={props.headerAction}>
|
||||
{(action) => <span class="tool-call-header-action">{action()}</span>}
|
||||
</Show>
|
||||
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="tool-call-header-copy"
|
||||
minItems={2}
|
||||
triggerClass="tool-call-header-icon-button tool-call-header-copy action-overflow-wide"
|
||||
minItems={1}
|
||||
/>
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems(true)}
|
||||
label={t("messageItem.actions.more")}
|
||||
triggerClass="tool-call-header-icon-button tool-call-header-copy action-overflow-narrow"
|
||||
minItems={1}
|
||||
/>
|
||||
|
||||
<ToolStatusIndicator status={status} />
|
||||
</div>
|
||||
|
||||
<Show when={expanded()}>
|
||||
|
|
@ -964,6 +1160,8 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
toolInput={toolInput}
|
||||
inputSectionExpanded={inputSectionExpanded}
|
||||
outputSectionExpanded={outputSectionExpanded}
|
||||
outputWrapEnabled={outputWrapEnabled}
|
||||
toggleOutputWrap={toggleOutputWrap}
|
||||
toggleInputSection={toggleInputSection}
|
||||
toggleOutputSection={toggleOutputSection}
|
||||
toolCallRootEl={toolCallRootEl}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export function createMarkdownContentRenderer(params: {
|
|||
|
||||
const size = options.size || "default"
|
||||
const disableHighlight = options.disableHighlight || false
|
||||
const messageClass = `message-text tool-call-markdown${size === "large" ? " tool-call-markdown-large" : ""}`
|
||||
const messageClass = `message-text tool-call-markdown${size === "large" ? " tool-call-markdown-large" : ""}${options.wrap ? " tool-call-markdown-wrap" : ""}`
|
||||
const state = params.toolState()
|
||||
const disableScrollTracking = options.disableScrollTracking || (state?.status !== "running" && state?.status !== "pending")
|
||||
const registerRef = disableScrollTracking ? registerUntracked : registerTracked
|
||||
|
|
|
|||
|
|
@ -15,24 +15,16 @@ export type PermissionToolBlockProps = {
|
|||
submitting: Accessor<boolean>
|
||||
error: Accessor<string | null>
|
||||
onRespond: (permission: PermissionRequest, sessionId: string, response: PermissionResponse, message?: string) => void | Promise<void>
|
||||
onRejectReasonOpenChange?: (open: boolean) => void
|
||||
renderDiff: (payload: DiffPayload, options?: DiffRenderOptions) => JSXElement | null
|
||||
fallbackSessionId: Accessor<string>
|
||||
}
|
||||
|
||||
export function PermissionToolBlock(props: PermissionToolBlockProps) {
|
||||
const { t } = useI18n()
|
||||
const [showRejectReason, setShowRejectReason] = createSignal(false)
|
||||
const [rejectReason, setRejectReason] = createSignal("")
|
||||
|
||||
const setRejectReasonOpen = (open: boolean) => {
|
||||
setShowRejectReason(open)
|
||||
props.onRejectReasonOpenChange?.(open)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
props.permission()?.id
|
||||
setRejectReasonOpen(false)
|
||||
setRejectReason("")
|
||||
})
|
||||
|
||||
|
|
@ -95,80 +87,50 @@ export function PermissionToolBlock(props: PermissionToolBlockProps) {
|
|||
<Show when={!props.active()}>
|
||||
<p class="tool-call-permission-queued-text">{t("toolCall.permission.queuedText")}</p>
|
||||
</Show>
|
||||
<Show
|
||||
when={showRejectReason()}
|
||||
fallback={
|
||||
<div class="tool-call-permission-actions">
|
||||
<div class="tool-call-permission-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={props.submitting()}
|
||||
onClick={() => respond("once")}
|
||||
>
|
||||
{t("toolCall.permission.actions.allowOnce")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={props.submitting()}
|
||||
onClick={() => respond("always")}
|
||||
>
|
||||
{t("toolCall.permission.actions.alwaysAllow")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={props.submitting()}
|
||||
onClick={() => setRejectReasonOpen(true)}
|
||||
>
|
||||
{t("toolCall.permission.actions.deny")}
|
||||
</button>
|
||||
</div>
|
||||
<Show when={props.active()}>
|
||||
<div class="tool-call-permission-shortcuts">
|
||||
<kbd class="kbd">Enter</kbd>
|
||||
<span>{t("toolCall.permission.shortcuts.allowOnce")}</span>
|
||||
<kbd class="kbd">A</kbd>
|
||||
<span>{t("toolCall.permission.shortcuts.alwaysAllow")}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="tool-call-permission-reject-reason">
|
||||
<label class="tool-call-permission-reject-label" for={`permission-reject-reason-${permission().id}`}>
|
||||
{t("toolCall.permission.rejectReason.label")}
|
||||
</label>
|
||||
<textarea
|
||||
id={`permission-reject-reason-${permission().id}`}
|
||||
class="tool-call-permission-reject-textarea"
|
||||
value={rejectReason()}
|
||||
rows={3}
|
||||
maxLength={PERMISSION_REJECT_REASON_MAX_LENGTH}
|
||||
placeholder={t("toolCall.permission.rejectReason.placeholder")}
|
||||
<div class="tool-call-permission-reject-reason">
|
||||
<textarea
|
||||
id={`permission-reject-reason-${permission().id}`}
|
||||
class="tool-call-permission-reject-textarea"
|
||||
value={rejectReason()}
|
||||
rows={1}
|
||||
maxLength={PERMISSION_REJECT_REASON_MAX_LENGTH}
|
||||
placeholder={t("toolCall.permission.rejectReason.placeholder")}
|
||||
aria-label={t("toolCall.permission.rejectReason.placeholder")}
|
||||
disabled={props.submitting()}
|
||||
onInput={(event) => setRejectReason(event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="tool-call-permission-actions">
|
||||
<div class="tool-call-permission-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={props.submitting()}
|
||||
onInput={(event) => setRejectReason(event.currentTarget.value)}
|
||||
/>
|
||||
<p class="tool-call-permission-reject-hint">{t("toolCall.permission.rejectReason.hint")}</p>
|
||||
<div class="tool-call-permission-buttons">
|
||||
<button type="button" class="tool-call-permission-button" disabled={props.submitting()} onClick={confirmReject}>
|
||||
{t("toolCall.permission.actions.confirmDeny")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={props.submitting()}
|
||||
onClick={() => {
|
||||
setRejectReasonOpen(false)
|
||||
setRejectReason("")
|
||||
}}
|
||||
>
|
||||
{t("toolCall.permission.actions.cancel")}
|
||||
</button>
|
||||
</div>
|
||||
onClick={() => respond("once")}
|
||||
>
|
||||
{t("toolCall.permission.actions.allowOnce")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={props.submitting()}
|
||||
onClick={() => respond("always")}
|
||||
>
|
||||
{t("toolCall.permission.actions.alwaysAllow")}
|
||||
</button>
|
||||
<button type="button" class="tool-call-permission-button" disabled={props.submitting()} onClick={confirmReject}>
|
||||
{t("toolCall.permission.actions.deny")}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.active()}>
|
||||
<div class="tool-call-permission-shortcuts">
|
||||
<kbd class="kbd">Enter</kbd>
|
||||
<span>{t("toolCall.permission.shortcuts.allowOnce")}</span>
|
||||
<kbd class="kbd">A</kbd>
|
||||
<span>{t("toolCall.permission.shortcuts.alwaysAllow")}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={props.error()}>
|
||||
<div class="tool-call-permission-error">{props.error()}</div>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@ export function QuestionToolBlock(props: QuestionToolBlockProps) {
|
|||
const customSelected = () => selected().filter((value) => !optionLabels().has(value))
|
||||
const customValue = () => customSelected()[0] ?? ""
|
||||
const customChecked = () => customValue().length > 0
|
||||
const showCustomAnswer = () => !hasFinalAnswers() || customChecked()
|
||||
|
||||
return (
|
||||
<div class="border border-base bg-surface-secondary p-3 text-primary">
|
||||
|
|
@ -249,57 +250,59 @@ export function QuestionToolBlock(props: QuestionToolBlockProps) {
|
|||
}}
|
||||
</For>
|
||||
|
||||
<label
|
||||
class={`mt-2 flex items-start gap-2 py-1 ${props.active() ? "cursor-pointer" : props.request() ? "opacity-80" : ""}`}
|
||||
title={t("toolCall.question.custom.title")}
|
||||
>
|
||||
<input
|
||||
ref={(el) => {
|
||||
if (i() === 0 && (q?.options?.length ?? 0) === 0) firstInputRef = el
|
||||
}}
|
||||
type={inputType()}
|
||||
name={groupName()}
|
||||
class="mt-0.5 accent-[var(--accent-primary)]"
|
||||
checked={customChecked()}
|
||||
disabled={!props.active() || props.submitting()}
|
||||
onChange={(e) => {
|
||||
const container = e.currentTarget.closest("label")
|
||||
const input = container?.querySelector("input[type='text']") as HTMLInputElement | null
|
||||
if (!props.active()) return
|
||||
if (customChecked()) {
|
||||
clearCustomAnswer(i(), customSelected())
|
||||
if (input) {
|
||||
delete input.dataset.lastValue
|
||||
}
|
||||
return
|
||||
}
|
||||
toggleFromCustomInput(i(), input)
|
||||
}}
|
||||
/>
|
||||
<div class="flex flex-1 flex-col gap-2">
|
||||
<div class="text-sm leading-tight text-primary">{t("toolCall.question.custom.label")}</div>
|
||||
<Show when={showCustomAnswer()}>
|
||||
<label
|
||||
class={`mt-2 flex items-start gap-2 py-1 ${props.active() ? "cursor-pointer" : props.request() ? "opacity-80" : ""}`}
|
||||
title={t("toolCall.question.custom.title")}
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-md border border-base bg-surface-base px-2 py-1 text-sm text-primary"
|
||||
type="text"
|
||||
placeholder={t("toolCall.question.custom.placeholder")}
|
||||
disabled={!props.active() || props.submitting()}
|
||||
value={customValue()}
|
||||
onFocus={(e) => {
|
||||
if (!props.active()) return
|
||||
// Keep the radio/checkbox selected while editing.
|
||||
toggleFromCustomInput(i(), e.currentTarget)
|
||||
ref={(el) => {
|
||||
if (i() === 0 && (q?.options?.length ?? 0) === 0) firstInputRef = el
|
||||
}}
|
||||
onInput={(e) => handleCustomTyping(i(), e.currentTarget)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.isComposing) {
|
||||
if (!submitDisabled()) {
|
||||
props.onSubmit()
|
||||
type={inputType()}
|
||||
name={groupName()}
|
||||
class="mt-0.5 accent-[var(--accent-primary)]"
|
||||
checked={customChecked()}
|
||||
disabled={!props.active() || props.submitting()}
|
||||
onChange={(e) => {
|
||||
const container = e.currentTarget.closest("label")
|
||||
const input = container?.querySelector("input[type='text']") as HTMLInputElement | null
|
||||
if (!props.active()) return
|
||||
if (customChecked()) {
|
||||
clearCustomAnswer(i(), customSelected())
|
||||
if (input) {
|
||||
delete input.dataset.lastValue
|
||||
}
|
||||
return
|
||||
}
|
||||
toggleFromCustomInput(i(), input)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<div class="flex flex-1 flex-col gap-2">
|
||||
<div class="text-sm leading-tight text-primary">{t("toolCall.question.custom.label")}</div>
|
||||
<input
|
||||
class="w-full rounded-md border border-base bg-surface-base px-2 py-1 text-sm text-primary"
|
||||
type="text"
|
||||
placeholder={t("toolCall.question.custom.placeholder")}
|
||||
disabled={!props.active() || props.submitting()}
|
||||
value={customValue()}
|
||||
onFocus={(e) => {
|
||||
if (!props.active()) return
|
||||
// Keep the radio/checkbox selected while editing.
|
||||
toggleFromCustomInput(i(), e.currentTarget)
|
||||
}}
|
||||
onInput={(e) => handleCustomTyping(i(), e.currentTarget)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.isComposing) {
|
||||
if (!submitDisabled()) {
|
||||
props.onSubmit()
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -62,6 +62,23 @@ export const applyPatchRenderer: ToolRenderer = {
|
|||
}
|
||||
return getToolName("apply_patch")
|
||||
},
|
||||
getOutputChrome({ toolState }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return undefined
|
||||
|
||||
const payload = readToolStatePayload(state)
|
||||
const files = Array.isArray((payload.metadata as any).files) ? ((payload.metadata as any).files as ApplyPatchFile[]) : []
|
||||
const diffs = files
|
||||
.map((file) => (typeof file.diff === "string" ? file.diff : typeof file.patch === "string" ? file.patch : ""))
|
||||
.filter((diff) => diff.trim().length > 0)
|
||||
if (diffs.length > 0) {
|
||||
return { language: "diff", copyText: diffs.join("\n"), suppressInnerHeader: false }
|
||||
}
|
||||
|
||||
const fallback = isToolStateCompleted(state) && typeof state.output === "string" ? state.output : null
|
||||
if (!fallback) return undefined
|
||||
return { language: "text", copyText: fallback, wrapToggle: true, suppressInnerHeader: true }
|
||||
},
|
||||
renderBody({ toolState, renderDiff, renderMarkdown, t }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return null
|
||||
|
|
|
|||
|
|
@ -10,17 +10,36 @@ import { getBashToolSearchText } from "../search-text"
|
|||
function RunningBashOutput(props: {
|
||||
content: Accessor<string>
|
||||
scrollHelpers?: ToolScrollHelpers
|
||||
onContentRendered?: () => void
|
||||
}) {
|
||||
let preRef: HTMLPreElement | undefined
|
||||
let pendingRenderNotificationFrame: number | null = null
|
||||
const updater = createStableAnsiStreamUpdater()
|
||||
|
||||
const notifyContentRendered = () => {
|
||||
if (!props.onContentRendered || typeof requestAnimationFrame !== "function") return
|
||||
if (pendingRenderNotificationFrame !== null) {
|
||||
cancelAnimationFrame(pendingRenderNotificationFrame)
|
||||
}
|
||||
pendingRenderNotificationFrame = requestAnimationFrame(() => {
|
||||
pendingRenderNotificationFrame = null
|
||||
props.onContentRendered?.()
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const element = preRef
|
||||
if (!element) return
|
||||
updater.update(element, props.content())
|
||||
props.scrollHelpers?.restoreAfterRender()
|
||||
notifyContentRendered()
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (pendingRenderNotificationFrame !== null) {
|
||||
cancelAnimationFrame(pendingRenderNotificationFrame)
|
||||
pendingRenderNotificationFrame = null
|
||||
}
|
||||
preRef = undefined
|
||||
updater.reset()
|
||||
})
|
||||
|
|
@ -37,10 +56,64 @@ function RunningBashOutput(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function FinalAnsiOutput(props: {
|
||||
html: string
|
||||
scrollHelpers?: ToolScrollHelpers
|
||||
onContentRendered?: () => void
|
||||
}) {
|
||||
let pendingRenderNotificationFrame: number | null = null
|
||||
|
||||
const notifyContentRendered = () => {
|
||||
if (!props.onContentRendered || typeof requestAnimationFrame !== "function") return
|
||||
if (pendingRenderNotificationFrame !== null) {
|
||||
cancelAnimationFrame(pendingRenderNotificationFrame)
|
||||
}
|
||||
pendingRenderNotificationFrame = requestAnimationFrame(() => {
|
||||
pendingRenderNotificationFrame = null
|
||||
props.onContentRendered?.()
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
props.html
|
||||
props.scrollHelpers?.restoreAfterRender()
|
||||
notifyContentRendered()
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (pendingRenderNotificationFrame !== null) {
|
||||
cancelAnimationFrame(pendingRenderNotificationFrame)
|
||||
pendingRenderNotificationFrame = null
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="message-text tool-call-markdown" ref={props.scrollHelpers?.registerContainer}>
|
||||
<pre class="tool-call-content tool-call-ansi" dir="auto" innerHTML={props.html} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getBashCopyText(state: ToolState | undefined): string {
|
||||
if (!state || state.status === "pending") return ""
|
||||
|
||||
const { input, metadata } = readToolStatePayload(state)
|
||||
const command = typeof input.command === "string" && input.command.length > 0 ? `$ ${input.command}` : ""
|
||||
const outputResult = formatUnknown(
|
||||
isToolStateCompleted(state)
|
||||
? state.output
|
||||
: (isToolStateRunning(state) || isToolStateError(state)) && metadata.output
|
||||
? metadata.output
|
||||
: undefined,
|
||||
)
|
||||
return [command, outputResult?.text].filter(Boolean).join("\n")
|
||||
}
|
||||
|
||||
function BashToolBody(props: {
|
||||
toolState: Accessor<ToolState | undefined>
|
||||
renderMarkdown: (options: { content: string }) => ReturnType<ToolRenderer["renderBody"]>
|
||||
scrollHelpers?: ToolScrollHelpers
|
||||
onContentRendered?: () => void
|
||||
}) {
|
||||
const state = createMemo(() => props.toolState())
|
||||
|
||||
|
|
@ -91,14 +164,12 @@ function BashToolBody(props: {
|
|||
fallback={
|
||||
<Show when={finalAnsiHtml()} fallback={finalMarkdown() ? props.renderMarkdown({ content: finalMarkdown()! as string }) : null}>
|
||||
{(html) => (
|
||||
<div class="message-text tool-call-markdown" ref={props.scrollHelpers?.registerContainer}>
|
||||
<pre class="tool-call-content tool-call-ansi" dir="auto" innerHTML={html()} />
|
||||
</div>
|
||||
<FinalAnsiOutput html={html()} scrollHelpers={props.scrollHelpers} onContentRendered={props.onContentRendered} />
|
||||
)}
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<RunningBashOutput content={joinedContent} scrollHelpers={props.scrollHelpers} />
|
||||
<RunningBashOutput content={joinedContent} scrollHelpers={props.scrollHelpers} onContentRendered={props.onContentRendered} />
|
||||
</Show>
|
||||
</Show>
|
||||
)
|
||||
|
|
@ -124,7 +195,12 @@ export const bashRenderer: ToolRenderer = {
|
|||
const timeoutLabel = `${timeout}ms`
|
||||
return `${baseTitle} · ${tGlobal("toolCall.renderer.bash.title.timeout", { timeout: timeoutLabel })}`
|
||||
},
|
||||
renderBody({ toolState, renderMarkdown, scrollHelpers }) {
|
||||
return <BashToolBody toolState={toolState} renderMarkdown={renderMarkdown as any} scrollHelpers={scrollHelpers} />
|
||||
getOutputChrome({ toolState }) {
|
||||
const text = getBashCopyText(toolState())
|
||||
if (!text) return undefined
|
||||
return { language: "bash", copyText: text, wrapToggle: true, suppressInnerHeader: true }
|
||||
},
|
||||
renderBody({ toolState, renderMarkdown, scrollHelpers, onContentRendered }) {
|
||||
return <BashToolBody toolState={toolState} renderMarkdown={renderMarkdown as any} scrollHelpers={scrollHelpers} onContentRendered={onContentRendered} />
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,28 @@ import { getDefaultToolSearchText } from "../search-text"
|
|||
export const defaultRenderer: ToolRenderer = {
|
||||
tools: ["*"],
|
||||
getSearchText: getDefaultToolSearchText,
|
||||
renderBody({ toolState, renderMarkdown }) {
|
||||
getOutputChrome({ toolState }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return undefined
|
||||
|
||||
const { metadata, input } = readToolStatePayload(state)
|
||||
const primaryOutput = isToolStateCompleted(state)
|
||||
? state.output
|
||||
: (isToolStateRunning(state) || isToolStateError(state)) && metadata.output
|
||||
? metadata.output
|
||||
: metadata.diff ?? metadata.preview ?? input.content
|
||||
|
||||
const result = formatUnknown(primaryOutput)
|
||||
if (!result) return undefined
|
||||
|
||||
return {
|
||||
language: result.language ?? "text",
|
||||
copyText: result.text,
|
||||
wrapToggle: true,
|
||||
suppressInnerHeader: true,
|
||||
}
|
||||
},
|
||||
renderBody({ toolState, renderMarkdown, outputWrapEnabled }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return null
|
||||
|
||||
|
|
@ -22,6 +43,6 @@ export const defaultRenderer: ToolRenderer = {
|
|||
const content = ensureMarkdownContent(result.text, result.language, true)
|
||||
if (!content) return null
|
||||
|
||||
return renderMarkdown({ content, disableHighlight: state.status === "running" })
|
||||
return renderMarkdown({ content, disableHighlight: state.status === "running", wrap: outputWrapEnabled?.() ?? true })
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,22 @@ export const editRenderer: ToolRenderer = {
|
|||
if (!filePath) return getToolName("edit")
|
||||
return `${getToolName("edit")} ${getRelativePath(filePath)}`
|
||||
},
|
||||
getOutputChrome({ toolState, toolName }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return undefined
|
||||
|
||||
const diffPayload = extractDiffPayload(toolName(), state)
|
||||
if (diffPayload) {
|
||||
return { language: "diff", copyText: diffPayload.diffText, suppressInnerHeader: false }
|
||||
}
|
||||
|
||||
const { metadata } = readToolStatePayload(state)
|
||||
const diffText = typeof metadata.diff === "string" ? metadata.diff : null
|
||||
const fallback = isToolStateCompleted(state) && typeof state.output === "string" ? state.output : null
|
||||
const copyText = diffText || fallback
|
||||
if (!copyText) return undefined
|
||||
return { language: "diff", copyText, wrapToggle: true, suppressInnerHeader: true }
|
||||
},
|
||||
renderBody({ toolState, toolName, renderDiff, renderMarkdown }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return null
|
||||
|
|
|
|||
|
|
@ -1,40 +1,2 @@
|
|||
import type { ToolRenderer } from "../types"
|
||||
import { bashRenderer } from "./bash"
|
||||
import { defaultRenderer } from "./default"
|
||||
import { editRenderer } from "./edit"
|
||||
import { applyPatchRenderer } from "./apply-patch"
|
||||
import { patchRenderer } from "./patch"
|
||||
import { readRenderer } from "./read"
|
||||
import { taskRenderer } from "./task"
|
||||
import { todoRenderer } from "./todo"
|
||||
import { webfetchRenderer } from "./webfetch"
|
||||
import { writeRenderer } from "./write"
|
||||
import { invalidRenderer } from "./invalid"
|
||||
import { questionRenderer } from "./question"
|
||||
|
||||
const TOOL_RENDERERS: ToolRenderer[] = [
|
||||
bashRenderer,
|
||||
readRenderer,
|
||||
writeRenderer,
|
||||
editRenderer,
|
||||
applyPatchRenderer,
|
||||
patchRenderer,
|
||||
webfetchRenderer,
|
||||
todoRenderer,
|
||||
taskRenderer,
|
||||
questionRenderer,
|
||||
invalidRenderer,
|
||||
]
|
||||
|
||||
const rendererMap = TOOL_RENDERERS.reduce<Record<string, ToolRenderer>>((acc, renderer) => {
|
||||
renderer.tools.forEach((tool) => {
|
||||
acc[tool] = renderer
|
||||
})
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
export function resolveToolRenderer(toolName: string): ToolRenderer {
|
||||
return rendererMap[toolName] ?? defaultRenderer
|
||||
}
|
||||
|
||||
export { defaultRenderer }
|
||||
export { resolveToolRenderer } from "../tool-registry"
|
||||
export { defaultRenderer } from "./default"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ export const invalidRenderer: ToolRenderer = {
|
|||
}
|
||||
return getToolName("invalid")
|
||||
},
|
||||
getOutputChrome(context) {
|
||||
return defaultRenderer.getOutputChrome?.(context)
|
||||
},
|
||||
renderBody(context) {
|
||||
return defaultRenderer.renderBody(context)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,22 @@ export const patchRenderer: ToolRenderer = {
|
|||
if (!filePath) return getToolName("patch")
|
||||
return `${getToolName("patch")} ${getRelativePath(filePath)}`
|
||||
},
|
||||
getOutputChrome({ toolState, toolName }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return undefined
|
||||
|
||||
const diffPayload = extractDiffPayload(toolName(), state)
|
||||
if (diffPayload) {
|
||||
return { language: "diff", copyText: diffPayload.diffText, suppressInnerHeader: false }
|
||||
}
|
||||
|
||||
const { metadata } = readToolStatePayload(state)
|
||||
const diffText = typeof metadata.diff === "string" ? metadata.diff : null
|
||||
const fallback = isToolStateCompleted(state) && typeof state.output === "string" ? state.output : null
|
||||
const copyText = diffText || fallback
|
||||
if (!copyText) return undefined
|
||||
return { language: "diff", copyText, wrapToggle: true, suppressInnerHeader: true }
|
||||
},
|
||||
renderBody({ toolState, toolName, renderDiff, renderMarkdown }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return null
|
||||
|
|
|
|||
|
|
@ -3,15 +3,23 @@ import { ensureMarkdownContent, getRelativePath, getToolName, inferLanguageFromP
|
|||
import { tGlobal } from "../../../lib/i18n"
|
||||
import { getReadToolSearchText } from "../search-text"
|
||||
|
||||
function getReadPath(input: Record<string, any>): string {
|
||||
const filePath = typeof input.filePath === "string" ? input.filePath : ""
|
||||
if (filePath) return filePath
|
||||
const path = typeof input.path === "string" ? input.path : ""
|
||||
if (path) return path
|
||||
return typeof input.name === "string" ? input.name : ""
|
||||
}
|
||||
|
||||
export const readRenderer: ToolRenderer = {
|
||||
tools: ["read"],
|
||||
getSearchText: getReadToolSearchText,
|
||||
getAction: () => tGlobal("toolCall.renderer.action.readingFile"),
|
||||
getTitle({ toolState }) {
|
||||
getTitle({ toolName, toolState }) {
|
||||
const state = toolState()
|
||||
if (!state) return undefined
|
||||
const { input } = readToolStatePayload(state)
|
||||
const filePath = typeof input.filePath === "string" ? input.filePath : ""
|
||||
const filePath = getReadPath(input)
|
||||
const offset = typeof input.offset === "number" ? input.offset : undefined
|
||||
const limit = typeof input.limit === "number" ? input.limit : undefined
|
||||
const relativePath = filePath ? getRelativePath(filePath) : ""
|
||||
|
|
@ -25,19 +33,29 @@ export const readRenderer: ToolRenderer = {
|
|||
detailParts.push(tGlobal("toolCall.renderer.read.detail.limit", { limit }))
|
||||
}
|
||||
|
||||
const baseTitle = relativePath ? `${getToolName("read")} ${relativePath}` : getToolName("read")
|
||||
const toolLabel = getToolName(toolName())
|
||||
const baseTitle = relativePath ? `${toolLabel} ${relativePath}` : toolLabel
|
||||
if (!detailParts.length) {
|
||||
return baseTitle
|
||||
}
|
||||
|
||||
return `${baseTitle} · ${detailParts.join(" · ")}`
|
||||
},
|
||||
getOutputChrome({ toolState }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return undefined
|
||||
const { metadata, input } = readToolStatePayload(state)
|
||||
const preview = typeof metadata.preview === "string" ? metadata.preview : null
|
||||
if (!preview) return undefined
|
||||
const language = inferLanguageFromPath(getReadPath(input)) ?? "text"
|
||||
return { language, copyText: preview, wrapToggle: true, suppressInnerHeader: true }
|
||||
},
|
||||
renderBody({ toolState, renderMarkdown }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return null
|
||||
const { metadata, input } = readToolStatePayload(state)
|
||||
const preview = typeof metadata.preview === "string" ? metadata.preview : null
|
||||
const language = inferLanguageFromPath(typeof input.filePath === "string" ? input.filePath : undefined)
|
||||
const language = inferLanguageFromPath(getReadPath(input))
|
||||
const content = ensureMarkdownContent(preview, language, true)
|
||||
if (!content) return null
|
||||
return renderMarkdown({ content, disableHighlight: state.status === "running" })
|
||||
|
|
|
|||
26
packages/ui/src/components/tool-call/renderers/search.tsx
Normal file
26
packages/ui/src/components/tool-call/renderers/search.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import type { ToolRenderer } from "../types"
|
||||
import { getToolName, readToolStatePayload } from "../utils"
|
||||
import { getDefaultToolSearchText } from "../search-text"
|
||||
import { defaultRenderer } from "./default"
|
||||
|
||||
export const searchRenderer: ToolRenderer = {
|
||||
tools: ["glob", "grep"],
|
||||
getSearchText: getDefaultToolSearchText,
|
||||
getTitle({ toolName, toolState }) {
|
||||
const state = toolState()
|
||||
const name = getToolName(toolName())
|
||||
if (!state) return name
|
||||
|
||||
const { input } = readToolStatePayload(state)
|
||||
const pattern = typeof input.pattern === "string" ? input.pattern.trim() : ""
|
||||
if (!pattern) return name
|
||||
|
||||
return `${name} ${pattern}`
|
||||
},
|
||||
getOutputChrome(context) {
|
||||
return defaultRenderer.getOutputChrome?.(context)
|
||||
},
|
||||
renderBody(context) {
|
||||
return defaultRenderer.renderBody(context)
|
||||
},
|
||||
}
|
||||
28
packages/ui/src/components/tool-call/renderers/skill.tsx
Normal file
28
packages/ui/src/components/tool-call/renderers/skill.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import type { ToolRenderer } from "../types"
|
||||
import { ensureMarkdownContent, formatUnknown, getToolName } from "../utils"
|
||||
import { getDefaultToolSearchText } from "../search-text"
|
||||
|
||||
export const skillRenderer: ToolRenderer = {
|
||||
tools: ["skill"],
|
||||
getSearchText: getDefaultToolSearchText,
|
||||
getTitle() {
|
||||
return getToolName("skill")
|
||||
},
|
||||
getOutputChrome({ toolState }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status !== "completed") return undefined
|
||||
|
||||
const output = formatUnknown(state.output)?.text ?? null
|
||||
if (!output) return undefined
|
||||
return { copyText: output, suppressInnerHeader: true }
|
||||
},
|
||||
renderBody({ toolState, renderMarkdown }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status !== "completed") return null
|
||||
|
||||
const output = formatUnknown(state.output)?.text ?? null
|
||||
const content = ensureMarkdownContent(output, undefined, false)
|
||||
if (!content) return null
|
||||
return <div class="tool-call-skill-body">{renderMarkdown({ content })}</div>
|
||||
},
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { For, Index, Show, createEffect, createMemo, createSignal, untrack } fro
|
|||
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||
import type { ToolRenderer } from "../types"
|
||||
import { ensureMarkdownContent, getDefaultToolAction, getToolIcon, getToolName, readToolStatePayload } from "../utils"
|
||||
import { resolveTitleForTool } from "../tool-title"
|
||||
import { messageStoreBus } from "../../../stores/message-v2/bus"
|
||||
import { loadMessages } from "../../../stores/session-api"
|
||||
import { loading, messagesLoaded } from "../../../stores/session-state"
|
||||
|
|
@ -121,6 +120,24 @@ function describeTaskTitle(input: Record<string, any>) {
|
|||
return base
|
||||
}
|
||||
|
||||
function describeGenericToolTitle(tool: string, input: Record<string, any>) {
|
||||
const base = getToolName(tool)
|
||||
const detail =
|
||||
typeof input.description === "string" && input.description.trim().length > 0
|
||||
? input.description.trim()
|
||||
: typeof input.filePath === "string" && input.filePath.trim().length > 0
|
||||
? input.filePath.trim()
|
||||
: typeof input.path === "string" && input.path.trim().length > 0
|
||||
? input.path.trim()
|
||||
: typeof input.url === "string" && input.url.trim().length > 0
|
||||
? input.url.trim()
|
||||
: typeof input.pattern === "string" && input.pattern.trim().length > 0
|
||||
? input.pattern.trim()
|
||||
: ""
|
||||
|
||||
return detail ? `${base} ${detail}` : base
|
||||
}
|
||||
|
||||
function describeToolTitle(item: TaskSummaryItem): string {
|
||||
if (item.title && item.title.length > 0) {
|
||||
return item.title
|
||||
|
|
@ -131,7 +148,12 @@ function describeToolTitle(item: TaskSummaryItem): string {
|
|||
}
|
||||
|
||||
if (item.state) {
|
||||
return resolveTitleForTool({ toolName: item.tool, state: item.state })
|
||||
const stateTitle = typeof (item.state as { title?: string }).title === "string" ? (item.state as { title?: string }).title : undefined
|
||||
if (stateTitle && stateTitle.length > 0) {
|
||||
return stateTitle
|
||||
}
|
||||
const { input } = readToolStatePayload(item.state)
|
||||
return describeGenericToolTitle(item.tool, { ...item.metadata, ...item.input, ...input })
|
||||
}
|
||||
|
||||
return getDefaultToolAction(item.tool)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { For, Show } from "solid-js"
|
||||
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||
import { CheckCircle, CircleEllipsis, MinusCircle, PauseCircle } from "lucide-solid"
|
||||
import type { ToolRenderer } from "../types"
|
||||
import { readToolStatePayload } from "../utils"
|
||||
import { useI18n, tGlobal } from "../../../lib/i18n"
|
||||
|
|
@ -60,6 +61,19 @@ function getTodoStatusLabel(t: (key: string) => string, status: TodoViewStatus):
|
|||
}
|
||||
}
|
||||
|
||||
function TodoStatusIcon(props: { status: TodoViewStatus }) {
|
||||
switch (props.status) {
|
||||
case "completed":
|
||||
return <CheckCircle class="tool-call-todo-icon" aria-hidden="true" />
|
||||
case "in_progress":
|
||||
return <CircleEllipsis class="tool-call-todo-icon" aria-hidden="true" />
|
||||
case "cancelled":
|
||||
return <MinusCircle class="tool-call-todo-icon" aria-hidden="true" />
|
||||
default:
|
||||
return <PauseCircle class="tool-call-todo-icon" aria-hidden="true" />
|
||||
}
|
||||
}
|
||||
|
||||
interface TodoListViewProps {
|
||||
state?: ToolState
|
||||
emptyLabel?: string
|
||||
|
|
@ -87,20 +101,20 @@ export function TodoListView(props: TodoListViewProps) {
|
|||
classList={{
|
||||
"tool-call-todo-item-completed": todo.status === "completed",
|
||||
"tool-call-todo-item-cancelled": todo.status === "cancelled",
|
||||
"tool-call-todo-item-active": todo.status === "in_progress",
|
||||
}}
|
||||
role="listitem"
|
||||
>
|
||||
<span class="tool-call-todo-checkbox" data-status={todo.status} aria-label={label}></span>
|
||||
<div class="tool-call-todo-body">
|
||||
<div class="tool-call-todo-heading">
|
||||
<span class="tool-call-todo-text">{todo.content}</span>
|
||||
<Show when={props.showStatusLabel !== false}>
|
||||
<span class={`tool-call-todo-status tool-call-todo-status-${todo.status}`}>{label}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<span class="tool-call-todo-checkbox" data-status={todo.status} aria-label={label}>
|
||||
<TodoStatusIcon status={todo.status} />
|
||||
</span>
|
||||
<div class="tool-call-todo-body">
|
||||
<div class="tool-call-todo-heading">
|
||||
<span class="tool-call-todo-text">{todo.content}</span>
|
||||
<Show when={props.showStatusLabel !== false}>
|
||||
<span class={`tool-call-todo-status tool-call-todo-status-${todo.status}`}>{label}</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
|
|
@ -123,7 +137,7 @@ export function getTodoTitle(state?: ToolState): string {
|
|||
}
|
||||
|
||||
export const todoRenderer: ToolRenderer = {
|
||||
tools: ["todowrite", "todoread"],
|
||||
tools: ["todowrite"],
|
||||
getSearchText: getTodoToolSearchText,
|
||||
getAction: () => tGlobal("toolCall.renderer.action.planning"),
|
||||
getTitle({ toolState }) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,25 @@ export const webfetchRenderer: ToolRenderer = {
|
|||
}
|
||||
return getToolName("webfetch")
|
||||
},
|
||||
getOutputChrome({ toolState }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return undefined
|
||||
|
||||
const { metadata } = readToolStatePayload(state)
|
||||
const result = formatUnknown(
|
||||
state.status === "completed"
|
||||
? state.output
|
||||
: metadata.output,
|
||||
)
|
||||
if (!result) return undefined
|
||||
|
||||
return {
|
||||
language: result.language ?? "text",
|
||||
copyText: result.text,
|
||||
wrapToggle: true,
|
||||
suppressInnerHeader: true,
|
||||
}
|
||||
},
|
||||
renderBody({ toolState, renderMarkdown }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return null
|
||||
|
|
|
|||
|
|
@ -15,6 +15,20 @@ export const writeRenderer: ToolRenderer = {
|
|||
if (!filePath) return getToolName("write")
|
||||
return `${getToolName("write")} ${getRelativePath(filePath)}`
|
||||
},
|
||||
getOutputChrome({ toolState }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return undefined
|
||||
const { metadata, input } = readToolStatePayload(state)
|
||||
const contentValue = typeof input.content === "string" ? input.content : metadata.content
|
||||
if (typeof contentValue !== "string" || contentValue.length === 0) return undefined
|
||||
const filePath = typeof input.filePath === "string" ? input.filePath : undefined
|
||||
return {
|
||||
language: inferLanguageFromPath(filePath) ?? "text",
|
||||
copyText: contentValue,
|
||||
wrapToggle: true,
|
||||
suppressInnerHeader: true,
|
||||
}
|
||||
},
|
||||
renderBody({ toolState, renderMarkdown }) {
|
||||
const state = toolState()
|
||||
if (!state || state.status === "pending") return null
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ export function getReadToolSearchText(context: ToolSearchTextContext): string[]
|
|||
const { input, metadata } = readToolStatePayload(context.toolState)
|
||||
appendBaseToolText(values, context)
|
||||
appendString(values, input.filePath)
|
||||
appendString(values, input.path)
|
||||
appendString(values, input.name)
|
||||
appendString(values, metadata.preview)
|
||||
appendToolErrorText(values, context)
|
||||
return values
|
||||
|
|
|
|||
204
packages/ui/src/components/tool-call/tool-registry.ts
Normal file
204
packages/ui/src/components/tool-call/tool-registry.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import type { ExpansionPreference, Preferences, ToolCallExpansionPreset } from "../../stores/preferences"
|
||||
import type { ToolRenderer } from "./types"
|
||||
import { applyPatchRenderer } from "./renderers/apply-patch"
|
||||
import { bashRenderer } from "./renderers/bash"
|
||||
import { defaultRenderer } from "./renderers/default"
|
||||
import { editRenderer } from "./renderers/edit"
|
||||
import { invalidRenderer } from "./renderers/invalid"
|
||||
import { patchRenderer } from "./renderers/patch"
|
||||
import { questionRenderer } from "./renderers/question"
|
||||
import { readRenderer } from "./renderers/read"
|
||||
import { searchRenderer } from "./renderers/search"
|
||||
import { skillRenderer } from "./renderers/skill"
|
||||
import { taskRenderer } from "./renderers/task"
|
||||
import { todoRenderer } from "./renderers/todo"
|
||||
import { webfetchRenderer } from "./renderers/webfetch"
|
||||
import { writeRenderer } from "./renderers/write"
|
||||
|
||||
export const OTHER_TOOL_NAME = "other"
|
||||
|
||||
export const THINKING_EXPANSION_PRESETS: Record<ToolCallExpansionPreset, ExpansionPreference> = {
|
||||
minimal: "collapsed",
|
||||
balanced: "collapsed",
|
||||
detailed: "expanded",
|
||||
everything: "expanded",
|
||||
}
|
||||
|
||||
export interface ToolRegistryEntry {
|
||||
tool: string
|
||||
label: string
|
||||
labelKey?: string
|
||||
renderer: ToolRenderer
|
||||
configurable: boolean
|
||||
expansionPresets: Record<ToolCallExpansionPreset, ExpansionPreference>
|
||||
aliases?: string[]
|
||||
}
|
||||
|
||||
const expanded = "expanded" satisfies ExpansionPreference
|
||||
const collapsed = "collapsed" satisfies ExpansionPreference
|
||||
|
||||
function presets(values: Record<ToolCallExpansionPreset, ExpansionPreference>) {
|
||||
return values
|
||||
}
|
||||
|
||||
export const TOOL_REGISTRY: ToolRegistryEntry[] = [
|
||||
{
|
||||
tool: "bash",
|
||||
label: "bash",
|
||||
renderer: bashRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "read",
|
||||
label: "read",
|
||||
renderer: readRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: collapsed, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "write",
|
||||
label: "write",
|
||||
renderer: writeRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "edit",
|
||||
label: "edit",
|
||||
renderer: editRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "patch",
|
||||
label: "patch",
|
||||
renderer: patchRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "apply_patch",
|
||||
label: "apply_patch",
|
||||
renderer: applyPatchRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "webfetch",
|
||||
label: "webfetch",
|
||||
renderer: webfetchRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: expanded, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "glob",
|
||||
label: "glob",
|
||||
renderer: searchRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: expanded, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "grep",
|
||||
label: "grep",
|
||||
renderer: searchRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: expanded, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "todowrite",
|
||||
label: "todowrite",
|
||||
renderer: todoRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: expanded, balanced: expanded, detailed: expanded, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "task",
|
||||
label: "task",
|
||||
renderer: taskRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "skill",
|
||||
label: "skill",
|
||||
renderer: skillRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: collapsed, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "question",
|
||||
label: "question",
|
||||
renderer: questionRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: "invalid",
|
||||
label: "invalid",
|
||||
renderer: invalidRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: collapsed, everything: expanded }),
|
||||
},
|
||||
{
|
||||
tool: OTHER_TOOL_NAME,
|
||||
label: "Other tools",
|
||||
labelKey: "settings.behavior.expansionDefaults.otherTools",
|
||||
renderer: defaultRenderer,
|
||||
configurable: true,
|
||||
expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: expanded, everything: expanded }),
|
||||
},
|
||||
]
|
||||
|
||||
const otherToolEntry = TOOL_REGISTRY.find((entry) => entry.tool === OTHER_TOOL_NAME)!
|
||||
|
||||
const registryMap = TOOL_REGISTRY.reduce<Record<string, ToolRegistryEntry>>((acc, entry) => {
|
||||
acc[entry.tool] = entry
|
||||
entry.aliases?.forEach((alias) => {
|
||||
acc[alias] = entry
|
||||
})
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
export function getToolRegistryEntry(toolName: string): ToolRegistryEntry {
|
||||
return registryMap[toolName] ?? otherToolEntry
|
||||
}
|
||||
|
||||
export function resolveToolRenderer(toolName: string): ToolRenderer {
|
||||
return getToolRegistryEntry(toolName).renderer
|
||||
}
|
||||
|
||||
export function getConfigurableToolEntries(): ToolRegistryEntry[] {
|
||||
return TOOL_REGISTRY.filter((entry) => entry.configurable)
|
||||
}
|
||||
|
||||
export function buildToolExpansionPresetDefaults(preset: ToolCallExpansionPreset): Record<string, ExpansionPreference> {
|
||||
const defaults: Record<string, ExpansionPreference> = {}
|
||||
for (const entry of getConfigurableToolEntries()) {
|
||||
defaults[entry.tool] = entry.expansionPresets[preset]
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
export function resolveToolExpansionDefault(preferences: Preferences, toolName: string): boolean {
|
||||
const entry = getToolRegistryEntry(toolName)
|
||||
const defaults = preferences.toolCallExpansionDefaults
|
||||
const presetMode = defaults.preset === "custom" ? undefined : entry.expansionPresets[defaults.preset]
|
||||
const otherPresetMode = defaults.preset === "custom" ? undefined : otherToolEntry.expansionPresets[defaults.preset]
|
||||
const mode = defaults.tools[entry.tool]
|
||||
?? presetMode
|
||||
?? defaults.tools[OTHER_TOOL_NAME]
|
||||
?? otherPresetMode
|
||||
?? preferences.toolOutputExpansion
|
||||
?? "expanded"
|
||||
return mode === "expanded"
|
||||
}
|
||||
|
||||
export function resolveThinkingExpansionDefault(preferences: Preferences): boolean {
|
||||
const defaults = preferences.toolCallExpansionDefaults
|
||||
const mode = defaults.thinking
|
||||
?? (defaults.preset === "custom" ? undefined : THINKING_EXPANSION_PRESETS[defaults.preset])
|
||||
?? preferences.thinkingBlocksExpansion
|
||||
?? "expanded"
|
||||
return mode === "expanded"
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||
import type { ToolRendererContext, ToolRenderer, ToolCallPart } from "./types"
|
||||
import { getDefaultToolAction, getToolName, isToolStateCompleted, isToolStateRunning } from "./utils"
|
||||
import { enMessages } from "../../lib/i18n/messages/en"
|
||||
import { defaultRenderer } from "./renderers/default"
|
||||
import { bashRenderer } from "./renderers/bash"
|
||||
import { readRenderer } from "./renderers/read"
|
||||
import { writeRenderer } from "./renderers/write"
|
||||
import { editRenderer } from "./renderers/edit"
|
||||
import { applyPatchRenderer } from "./renderers/apply-patch"
|
||||
import { patchRenderer } from "./renderers/patch"
|
||||
import { webfetchRenderer } from "./renderers/webfetch"
|
||||
import { todoRenderer } from "./renderers/todo"
|
||||
import { invalidRenderer } from "./renderers/invalid"
|
||||
|
||||
const TITLE_RENDERERS: Record<string, ToolRenderer> = {
|
||||
bash: bashRenderer,
|
||||
read: readRenderer,
|
||||
write: writeRenderer,
|
||||
edit: editRenderer,
|
||||
apply_patch: applyPatchRenderer,
|
||||
patch: patchRenderer,
|
||||
webfetch: webfetchRenderer,
|
||||
todowrite: todoRenderer,
|
||||
todoread: todoRenderer,
|
||||
invalid: invalidRenderer,
|
||||
}
|
||||
|
||||
interface TitleSnapshot {
|
||||
toolName: string
|
||||
state?: ToolState
|
||||
}
|
||||
|
||||
function lookupRenderer(toolName: string): ToolRenderer {
|
||||
return TITLE_RENDERERS[toolName] ?? defaultRenderer
|
||||
}
|
||||
|
||||
function createStaticToolPart(snapshot: TitleSnapshot): ToolCallPart {
|
||||
return {
|
||||
id: "",
|
||||
type: "tool",
|
||||
tool: snapshot.toolName,
|
||||
state: snapshot.state,
|
||||
} as ToolCallPart
|
||||
}
|
||||
|
||||
function interpolate(template: string, params?: Record<string, unknown>): string {
|
||||
if (!params) return template
|
||||
return template.replace(/\{(\w+)\}/g, (_match, key: string) => {
|
||||
const value = params[key]
|
||||
return value === undefined || value === null ? "" : String(value)
|
||||
})
|
||||
}
|
||||
|
||||
function createStaticT(): ToolRendererContext["t"] {
|
||||
return (key, params) => {
|
||||
const template = (enMessages as Record<string, string>)[key] ?? key
|
||||
return interpolate(template, params)
|
||||
}
|
||||
}
|
||||
|
||||
function createStaticContext(snapshot: TitleSnapshot): ToolRendererContext {
|
||||
const toolStateAccessor = () => snapshot.state
|
||||
const toolNameAccessor = () => snapshot.toolName
|
||||
const toolCallAccessor = () => createStaticToolPart(snapshot)
|
||||
const messageVersionAccessor = () => undefined
|
||||
const partVersionAccessor = () => undefined
|
||||
const t = createStaticT()
|
||||
const renderMarkdown: ToolRendererContext["renderMarkdown"] = () => null
|
||||
const renderAnsi: ToolRendererContext["renderAnsi"] = () => null
|
||||
const renderDiff: ToolRendererContext["renderDiff"] = () => null
|
||||
|
||||
return {
|
||||
toolCall: toolCallAccessor,
|
||||
toolState: toolStateAccessor,
|
||||
toolName: toolNameAccessor,
|
||||
instanceId: "",
|
||||
sessionId: "",
|
||||
t,
|
||||
messageVersion: messageVersionAccessor,
|
||||
partVersion: partVersionAccessor,
|
||||
renderMarkdown,
|
||||
renderAnsi,
|
||||
renderDiff,
|
||||
renderToolCall: () => null,
|
||||
scrollHelpers: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveTitleForTool(snapshot: TitleSnapshot): string {
|
||||
const renderer = lookupRenderer(snapshot.toolName)
|
||||
const context = createStaticContext(snapshot)
|
||||
const state = snapshot.state
|
||||
const defaultAction = renderer.getAction?.(context) ?? getDefaultToolAction(snapshot.toolName)
|
||||
|
||||
if (!state || state.status === "pending") {
|
||||
return defaultAction
|
||||
}
|
||||
|
||||
const stateTitle = typeof (state as { title?: string }).title === "string" ? (state as { title?: string }).title : undefined
|
||||
if (stateTitle && stateTitle.length > 0) {
|
||||
return stateTitle
|
||||
}
|
||||
|
||||
const customTitle = renderer.getTitle?.(context)
|
||||
if (customTitle) {
|
||||
return customTitle
|
||||
}
|
||||
|
||||
return getToolName(snapshot.toolName)
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ export interface MarkdownRenderOptions {
|
|||
content: string
|
||||
size?: "default" | "large"
|
||||
disableHighlight?: boolean
|
||||
wrap?: boolean
|
||||
/**
|
||||
* Optional suffix to avoid render-cache collisions when a tool call renders
|
||||
* multiple markdown regions (e.g. task prompt vs task output).
|
||||
|
|
@ -74,6 +75,7 @@ export interface ToolRendererContext {
|
|||
sessionId: string
|
||||
forceCollapsed?: boolean
|
||||
}) => JSXElement | null
|
||||
outputWrapEnabled?: Accessor<boolean>
|
||||
scrollHelpers?: ToolScrollHelpers
|
||||
onContentRendered?: () => void
|
||||
}
|
||||
|
|
@ -88,6 +90,7 @@ export interface ToolRenderer {
|
|||
tools: string[]
|
||||
getTitle?(context: ToolRendererContext): string | undefined
|
||||
getAction?(context: ToolRendererContext): string | undefined
|
||||
getOutputChrome?(context: ToolRendererContext): ToolOutputChrome | undefined
|
||||
/**
|
||||
* Text that is visible or directly revealable through this renderer. Keep this
|
||||
* in sync with custom renderBody output when adding specialized tool UIs.
|
||||
|
|
@ -96,4 +99,13 @@ export interface ToolRenderer {
|
|||
renderBody(context: ToolRendererContext): JSXElement | null
|
||||
}
|
||||
|
||||
export interface ToolOutputChrome {
|
||||
title?: string
|
||||
language?: string
|
||||
copyText?: string | null
|
||||
actions?: JSXElement
|
||||
wrapToggle?: boolean
|
||||
suppressInnerHeader?: boolean
|
||||
}
|
||||
|
||||
export type ToolRendererMap = Record<string, ToolRenderer>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ export function getToolIcon(tool: string): string {
|
|||
case "task":
|
||||
return "🎯"
|
||||
case "todowrite":
|
||||
case "todoread":
|
||||
return "📋"
|
||||
case "question":
|
||||
return "❓"
|
||||
|
|
@ -68,7 +67,6 @@ export function getToolName(tool: string): string {
|
|||
case "invalid":
|
||||
return tGlobal("toolCall.renderer.toolName.invalid")
|
||||
case "todowrite":
|
||||
case "todoread":
|
||||
return tGlobal("toolCall.renderer.toolName.plan")
|
||||
case "apply_patch":
|
||||
return tGlobal("toolCall.renderer.toolName.applyPatch")
|
||||
|
|
@ -221,7 +219,6 @@ export function getDefaultToolAction(toolName: string) {
|
|||
case "write":
|
||||
return tGlobal("toolCall.renderer.action.preparingWrite")
|
||||
case "todowrite":
|
||||
case "todoread":
|
||||
return tGlobal("toolCall.renderer.action.planning")
|
||||
case "patch":
|
||||
return tGlobal("toolCall.renderer.action.preparingPatch")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import {
|
|||
VirtualScrollController,
|
||||
isAtBottom,
|
||||
isAutoFollowing,
|
||||
resolveAutoPinHoldElement,
|
||||
shouldSuspendAutoPinToBottomForHold,
|
||||
transitionFollowMode,
|
||||
type FollowMode,
|
||||
type ScrollControllerMetrics,
|
||||
|
|
@ -37,11 +39,11 @@ describe("virtual follow behavior", () => {
|
|||
assert.deepEqual(next.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("rejoins follow and pins bottom when escaped user scrolls down with pin permission", () => {
|
||||
it("does not rejoin follow above bottom even with pin permission", () => {
|
||||
const next = transitionFollowMode({ type: "escaped" }, userScroll("down", false, true))
|
||||
|
||||
assert.deepEqual(next.mode, { type: "following" })
|
||||
assert.deepEqual(next.effect, { type: "scroll-bottom", immediate: true, suppressHold: false })
|
||||
assert.deepEqual(next.mode, { type: "escaped" })
|
||||
assert.deepEqual(next.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("rejoins follow when escaped user scrolls to the bottom", () => {
|
||||
|
|
@ -51,10 +53,10 @@ describe("virtual follow behavior", () => {
|
|||
assert.deepEqual(next.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("releases hold when the user scrolls down above bottom", () => {
|
||||
it("keeps hold latched when the user scrolls down above bottom", () => {
|
||||
const next = transitionFollowMode({ type: "holding", key: "message-1" }, userScroll("down", false, true))
|
||||
|
||||
assert.deepEqual(next.mode, { type: "escaped" })
|
||||
assert.deepEqual(next.mode, { type: "holding", key: "message-1" })
|
||||
assert.deepEqual(next.effect, { type: "none" })
|
||||
})
|
||||
|
||||
|
|
@ -72,10 +74,10 @@ describe("virtual follow behavior", () => {
|
|||
assert.deepEqual(next.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("releases hold for directionless user scroll away from bottom", () => {
|
||||
it("keeps hold latched for directionless user scroll away from bottom", () => {
|
||||
const next = transitionFollowMode({ type: "holding", key: "message-1" }, userScroll(null, false, true))
|
||||
|
||||
assert.deepEqual(next.mode, { type: "escaped" })
|
||||
assert.deepEqual(next.mode, { type: "holding", key: "message-1" })
|
||||
assert.deepEqual(next.effect, { type: "none" })
|
||||
})
|
||||
|
||||
|
|
@ -87,11 +89,11 @@ describe("virtual follow behavior", () => {
|
|||
assert.deepEqual(following.effect, { type: "scroll-bottom", immediate: true, suppressHold: false })
|
||||
})
|
||||
|
||||
it("maintains hold alignment while held content grows", () => {
|
||||
it("does not align or pin while held content grows", () => {
|
||||
const next = transitionFollowMode({ type: "holding", key: "message-1" }, { type: "content-grew", canPinToBottom: true })
|
||||
|
||||
assert.deepEqual(next.mode, { type: "holding", key: "message-1" })
|
||||
assert.deepEqual(next.effect, { type: "align-hold", key: "message-1" })
|
||||
assert.deepEqual(next.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("enters hold mode for a valid hold candidate", () => {
|
||||
|
|
@ -101,6 +103,20 @@ describe("virtual follow behavior", () => {
|
|||
assert.deepEqual(next.effect, { type: "align-hold", key: "message-1" })
|
||||
})
|
||||
|
||||
it("keeps hold latched when the hold target disappears", () => {
|
||||
const next = transitionFollowMode({ type: "holding", key: "message-1" }, { type: "hold-target-changed", key: null, canPinToBottom: true })
|
||||
|
||||
assert.deepEqual(next.mode, { type: "holding", key: "message-1" })
|
||||
assert.deepEqual(next.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("keeps hold latched when a later hold target is reported", () => {
|
||||
const next = transitionFollowMode({ type: "holding", key: "message-1" }, { type: "hold-target-changed", key: "message-2", canPinToBottom: true })
|
||||
|
||||
assert.deepEqual(next.mode, { type: "holding", key: "message-1" })
|
||||
assert.deepEqual(next.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("explicit bottom jumps leave hold and suppress the next hold", () => {
|
||||
const next = transitionFollowMode({ type: "holding", key: "message-1" }, { type: "jump-bottom", immediate: true, explicit: true })
|
||||
|
||||
|
|
@ -108,6 +124,80 @@ describe("virtual follow behavior", () => {
|
|||
assert.deepEqual(next.effect, { type: "scroll-bottom", immediate: true, suppressHold: true })
|
||||
})
|
||||
|
||||
it("prompt submission overrides a stale hold latch and returns to bottom follow", () => {
|
||||
const controller = new VirtualScrollController(true)
|
||||
controller.holdCandidate("old-assistant-answer", true)
|
||||
|
||||
const result = controller.jumpBottom(true, true)
|
||||
|
||||
assert.deepEqual(result.state.mode, { type: "following" })
|
||||
assert.deepEqual(result.effect, { type: "scroll-bottom", immediate: true, suppressHold: true })
|
||||
assert.equal(controller.isAutoFollowing(), true)
|
||||
})
|
||||
|
||||
it("clears an existing hold latch when hold targeting is disabled", () => {
|
||||
const controller = new VirtualScrollController(true)
|
||||
controller.holdCandidate("old-assistant-answer", true)
|
||||
|
||||
const result = controller.clearHold(true, true, true)
|
||||
|
||||
assert.deepEqual(result.state.mode, { type: "following" })
|
||||
assert.deepEqual(result.effect, { type: "scroll-bottom", immediate: true, suppressHold: true })
|
||||
})
|
||||
|
||||
it("keeps submitted prompt content growth in bottom-follow after clearing stale hold", () => {
|
||||
const controller = new VirtualScrollController(true)
|
||||
controller.holdCandidate("old-assistant-answer", true)
|
||||
controller.jumpBottom(true, true)
|
||||
|
||||
const result = controller.contentRendered(metrics(2400), true)
|
||||
|
||||
assert.deepEqual(result.state.mode, { type: "following" })
|
||||
assert.deepEqual(result.effect, { type: "scroll-bottom", immediate: true, suppressHold: false })
|
||||
})
|
||||
|
||||
it("ignores stale previous assistant hold target changes after a submit bottom jump", () => {
|
||||
const controller = new VirtualScrollController(true)
|
||||
controller.holdCandidate("previous-assistant-answer", true)
|
||||
controller.jumpBottom(true, true)
|
||||
|
||||
const targetChanged = controller.holdTargetChanged("previous-assistant-answer", true)
|
||||
const contentRendered = controller.contentRendered(metrics(2400), true)
|
||||
|
||||
assert.deepEqual(targetChanged.state.mode, { type: "following" })
|
||||
assert.deepEqual(targetChanged.effect, { type: "none" })
|
||||
assert.deepEqual(contentRendered.state.mode, { type: "following" })
|
||||
assert.deepEqual(contentRendered.effect, { type: "scroll-bottom", immediate: true, suppressHold: false })
|
||||
})
|
||||
|
||||
it("keeps escaped-mode streaming detached until actual bottom", () => {
|
||||
const suspend = shouldSuspendAutoPinToBottomForHold({
|
||||
externalSuspend: false,
|
||||
activeHoldTargetKey: null,
|
||||
eligibleHoldTargetKey: "streaming-assistant-answer",
|
||||
})
|
||||
|
||||
const next = transitionFollowMode({ type: "escaped" }, userScroll("down", false, !suspend))
|
||||
|
||||
assert.equal(suspend, false)
|
||||
assert.deepEqual(next.mode, { type: "escaped" })
|
||||
assert.deepEqual(next.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("keeps auto-pin suspended while a hold target is actively latched", () => {
|
||||
const suspend = shouldSuspendAutoPinToBottomForHold({
|
||||
externalSuspend: false,
|
||||
activeHoldTargetKey: "streaming-assistant-answer",
|
||||
eligibleHoldTargetKey: "streaming-assistant-answer",
|
||||
})
|
||||
|
||||
const next = transitionFollowMode({ type: "holding", key: "streaming-assistant-answer" }, userScroll("down", false, !suspend))
|
||||
|
||||
assert.equal(suspend, true)
|
||||
assert.deepEqual(next.mode, { type: "holding", key: "streaming-assistant-answer" })
|
||||
assert.deepEqual(next.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("key jumps can opt into follow or escape mode", () => {
|
||||
const follow = transitionFollowMode({ type: "escaped" }, { type: "jump-key", key: "a", block: "start", smooth: false, followAfter: true })
|
||||
const escape = transitionFollowMode({ type: "following" }, { type: "jump-key", key: "b", block: "center", smooth: true, followAfter: false })
|
||||
|
|
@ -119,7 +209,7 @@ describe("virtual follow behavior", () => {
|
|||
it("derives auto-follow from modes", () => {
|
||||
const modes: Array<[FollowMode, boolean]> = [
|
||||
[{ type: "following" }, true],
|
||||
[{ type: "holding", key: "message-1" }, true],
|
||||
[{ type: "holding", key: "message-1" }, false],
|
||||
[{ type: "escaped" }, false],
|
||||
]
|
||||
|
||||
|
|
@ -138,14 +228,24 @@ describe("virtual follow behavior", () => {
|
|||
assert.deepEqual(result.effect, { type: "scroll-bottom", immediate: true, suppressHold: false })
|
||||
})
|
||||
|
||||
it("maintains hold alignment when held content renders", () => {
|
||||
it("does not align or pin when held content renders", () => {
|
||||
const controller = new VirtualScrollController(true)
|
||||
controller.holdCandidate("message-1", true)
|
||||
|
||||
const result = controller.contentRendered(metrics(2200), true)
|
||||
|
||||
assert.deepEqual(result.state.mode, { type: "holding", key: "message-1" })
|
||||
assert.deepEqual(result.effect, { type: "align-hold", key: "message-1" })
|
||||
assert.deepEqual(result.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("does not resume or snap when a held target disappears", () => {
|
||||
const controller = new VirtualScrollController(true)
|
||||
controller.holdCandidate("message-1", true)
|
||||
|
||||
const result = controller.holdTargetChanged(null, true)
|
||||
|
||||
assert.deepEqual(result.state.mode, { type: "holding", key: "message-1" })
|
||||
assert.deepEqual(result.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("lets fresh user upward movement escape even during a programmatic window", () => {
|
||||
|
|
@ -189,6 +289,17 @@ describe("virtual follow behavior", () => {
|
|||
assert.deepEqual(result.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("does not magnet to bottom above bottom even with integration pin permission", () => {
|
||||
const controller = new VirtualScrollController(false)
|
||||
controller.recordProgrammaticOffset(2100, false)
|
||||
controller.setUserIntent("down", 700)
|
||||
|
||||
const result = controller.observeViewport(metrics(2220), 100, false, true)
|
||||
|
||||
assert.deepEqual(result.state.mode, { type: "escaped" })
|
||||
assert.deepEqual(result.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("resumes follow only when downward movement reaches actual bottom", () => {
|
||||
const controller = new VirtualScrollController(false)
|
||||
controller.recordProgrammaticOffset(2300, false)
|
||||
|
|
@ -200,6 +311,24 @@ describe("virtual follow behavior", () => {
|
|||
assert.deepEqual(result.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("keeps hold latched until downward movement reaches actual bottom", () => {
|
||||
const controller = new VirtualScrollController(true)
|
||||
controller.holdCandidate("message-1", true)
|
||||
controller.recordProgrammaticOffset(2100, false)
|
||||
controller.setUserIntent("down", 700)
|
||||
|
||||
const nearBottom = controller.observeViewport(metrics(2220), 100, false, true)
|
||||
|
||||
assert.deepEqual(nearBottom.state.mode, { type: "holding", key: "message-1" })
|
||||
assert.deepEqual(nearBottom.effect, { type: "none" })
|
||||
|
||||
controller.setUserIntent("down", 800)
|
||||
const atBottom = controller.observeViewport(metrics(2400), 200, false, true)
|
||||
|
||||
assert.deepEqual(atBottom.state.mode, { type: "following" })
|
||||
assert.deepEqual(atBottom.effect, { type: "none" })
|
||||
})
|
||||
|
||||
it("still escapes follow on upward movement at bottom", () => {
|
||||
const controller = new VirtualScrollController(true)
|
||||
controller.recordProgrammaticOffset(1200, false)
|
||||
|
|
@ -245,4 +374,13 @@ describe("virtual follow behavior", () => {
|
|||
|
||||
assert.equal(isAtBottom(closeButNotAtBottom), false)
|
||||
})
|
||||
|
||||
it("excludes reasoning-only hold targets while preserving Assistant text eligibility", () => {
|
||||
const itemWrapper = { id: "message-wrapper" } as unknown as HTMLElement
|
||||
const assistantAnswerText = { id: "assistant-answer-text" } as unknown as HTMLElement
|
||||
|
||||
assert.equal(resolveAutoPinHoldElement(itemWrapper, "message-1", () => null), null)
|
||||
assert.equal(resolveAutoPinHoldElement(itemWrapper, "message-1", () => assistantAnswerText), assistantAnswerText)
|
||||
assert.equal(resolveAutoPinHoldElement(itemWrapper, "message-1", () => undefined), itemWrapper)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type FollowEvent =
|
|||
| { type: "content-grew"; canPinToBottom: boolean }
|
||||
| { type: "hold-candidate"; key: string; shouldHold: boolean }
|
||||
| { type: "hold-target-changed"; key: string | null; canPinToBottom: boolean }
|
||||
| { type: "clear-hold"; follow: boolean; canPinToBottom: boolean; suppressHold: boolean }
|
||||
| { type: "set-follow"; enabled: boolean }
|
||||
| { type: "reset"; follow: boolean }
|
||||
|
||||
|
|
@ -58,35 +59,107 @@ export interface ScrollControllerSnapshot {
|
|||
restoring: boolean
|
||||
}
|
||||
|
||||
export interface FollowSnapshotState {
|
||||
followModeType?: FollowMode["type"]
|
||||
heldKey?: string
|
||||
holdAnchorSuspended?: boolean
|
||||
}
|
||||
|
||||
export type HoldTargetElementResolver = (itemWrapper: HTMLElement, key: string) => HTMLElement | null | undefined
|
||||
|
||||
const noFollowEffect: FollowEffect = { type: "none" }
|
||||
|
||||
export function isAutoFollowing(mode: FollowMode) {
|
||||
return mode.type === "following" || mode.type === "holding"
|
||||
return mode.type === "following"
|
||||
}
|
||||
|
||||
export function getHeldKey(mode: FollowMode) {
|
||||
return mode.type === "holding" ? mode.key : null
|
||||
}
|
||||
|
||||
export function resolveAutoPinHoldElement(
|
||||
itemWrapper: HTMLElement | null | undefined,
|
||||
key: string,
|
||||
resolver?: HoldTargetElementResolver,
|
||||
) {
|
||||
if (!itemWrapper) return null
|
||||
if (!resolver) return itemWrapper
|
||||
|
||||
const resolved = resolver(itemWrapper, key)
|
||||
return resolved === undefined ? itemWrapper : resolved
|
||||
}
|
||||
|
||||
export function shouldSuspendAutoPinToBottomForHold(state: {
|
||||
externalSuspend: boolean
|
||||
activeHoldTargetKey: string | null
|
||||
eligibleHoldTargetKey?: string | null
|
||||
}) {
|
||||
return state.externalSuspend || state.activeHoldTargetKey !== null
|
||||
}
|
||||
|
||||
export function shouldTrackHeldAnchor(state: {
|
||||
activeHoldTargetKey: string | null
|
||||
eligibleHoldTargetKey: string | null
|
||||
suspendedByUser: boolean
|
||||
}) {
|
||||
return !state.suspendedByUser && state.activeHoldTargetKey !== null && state.activeHoldTargetKey === state.eligibleHoldTargetKey
|
||||
}
|
||||
|
||||
export function isSnapshotAutoFollowing(snapshot: { atBottom: boolean; followModeType?: FollowMode["type"] } | null | undefined) {
|
||||
if (!snapshot) return true
|
||||
if (snapshot.followModeType) return snapshot.followModeType === "following"
|
||||
return snapshot.atBottom
|
||||
}
|
||||
|
||||
export function getFollowSnapshotState(mode: FollowMode, holdAnchorSuspended: boolean): FollowSnapshotState {
|
||||
if (mode.type === "holding") {
|
||||
return {
|
||||
followModeType: "holding",
|
||||
heldKey: mode.key,
|
||||
holdAnchorSuspended: holdAnchorSuspended || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
return { followModeType: mode.type }
|
||||
}
|
||||
|
||||
export function restoreFollowModeFromSnapshot(state: {
|
||||
atBottom: boolean
|
||||
followModeType?: FollowMode["type"]
|
||||
heldKey?: string
|
||||
hasHeldKeyItem?: boolean
|
||||
}): FollowMode {
|
||||
if (state.followModeType === "holding" && state.heldKey && state.hasHeldKeyItem) {
|
||||
return { type: "holding", key: state.heldKey }
|
||||
}
|
||||
if (state.followModeType === "following") {
|
||||
return { type: "following" }
|
||||
}
|
||||
if (state.followModeType === "escaped") {
|
||||
return { type: "escaped" }
|
||||
}
|
||||
return state.atBottom ? { type: "following" } : { type: "escaped" }
|
||||
}
|
||||
|
||||
export function transitionFollowMode(mode: FollowMode, event: FollowEvent): FollowTransition {
|
||||
switch (event.type) {
|
||||
case "user-scroll": {
|
||||
if (mode.type === "holding") {
|
||||
if (event.atBottom && event.direction !== "up") {
|
||||
return { mode: { type: "following" }, effect: noFollowEffect }
|
||||
}
|
||||
return { mode, effect: noFollowEffect }
|
||||
}
|
||||
if (event.direction === "up") {
|
||||
return { mode: { type: "escaped" }, effect: noFollowEffect }
|
||||
}
|
||||
if (mode.type === "holding" && event.direction === null) {
|
||||
return { mode: { type: "escaped" }, effect: noFollowEffect }
|
||||
}
|
||||
if (mode.type === "holding" && event.direction === "down") {
|
||||
return { mode: { type: "escaped" }, effect: noFollowEffect }
|
||||
}
|
||||
if (mode.type === "escaped" && event.direction === "down" && event.canPinToBottom) {
|
||||
if (mode.type === "escaped" && event.direction === "down" && event.atBottom && event.canPinToBottom) {
|
||||
return {
|
||||
mode: { type: "following" },
|
||||
effect: { type: "scroll-bottom", immediate: true, suppressHold: false },
|
||||
}
|
||||
}
|
||||
if (event.atBottom && mode.type !== "holding") {
|
||||
if (event.atBottom) {
|
||||
return { mode: { type: "following" }, effect: noFollowEffect }
|
||||
}
|
||||
return { mode, effect: noFollowEffect }
|
||||
|
|
@ -111,9 +184,6 @@ export function transitionFollowMode(mode: FollowMode, event: FollowEvent): Foll
|
|||
if (mode.type === "following" && event.canPinToBottom) {
|
||||
return { mode, effect: { type: "scroll-bottom", immediate: true, suppressHold: false } }
|
||||
}
|
||||
if (mode.type === "holding" && event.canPinToBottom) {
|
||||
return { mode, effect: { type: "align-hold", key: mode.key } }
|
||||
}
|
||||
return { mode, effect: noFollowEffect }
|
||||
|
||||
case "hold-candidate":
|
||||
|
|
@ -123,12 +193,18 @@ export function transitionFollowMode(mode: FollowMode, event: FollowEvent): Foll
|
|||
return { mode, effect: noFollowEffect }
|
||||
|
||||
case "hold-target-changed":
|
||||
if (mode.type !== "holding" || event.key === mode.key) {
|
||||
return { mode, effect: noFollowEffect }
|
||||
|
||||
case "clear-hold":
|
||||
if (mode.type !== "holding") {
|
||||
return { mode, effect: noFollowEffect }
|
||||
}
|
||||
return {
|
||||
mode: { type: "following" },
|
||||
effect: event.canPinToBottom ? { type: "scroll-bottom", immediate: false, suppressHold: false } : noFollowEffect,
|
||||
mode: event.follow ? { type: "following" } : { type: "escaped" },
|
||||
effect:
|
||||
event.follow && event.canPinToBottom
|
||||
? { type: "scroll-bottom", immediate: true, suppressHold: event.suppressHold }
|
||||
: noFollowEffect,
|
||||
}
|
||||
|
||||
case "set-follow":
|
||||
|
|
@ -205,6 +281,11 @@ export class VirtualScrollController {
|
|||
return this.result(next.effect)
|
||||
}
|
||||
|
||||
restoreMode(mode: FollowMode): ScrollControllerResult {
|
||||
this.state.mode = mode
|
||||
return this.result(noFollowEffect)
|
||||
}
|
||||
|
||||
jumpTop(immediate: boolean): ScrollControllerResult {
|
||||
const next = transitionFollowMode(this.state.mode, { type: "jump-top", immediate })
|
||||
this.state.mode = next.mode
|
||||
|
|
@ -235,7 +316,19 @@ export class VirtualScrollController {
|
|||
return this.result(next.effect)
|
||||
}
|
||||
|
||||
observeViewport(metrics: ScrollControllerMetrics, now: number, programmatic: boolean, canPinToBottom = false): ScrollControllerResult {
|
||||
clearHold(follow: boolean, canPinToBottom: boolean, suppressHold: boolean): ScrollControllerResult {
|
||||
const next = transitionFollowMode(this.state.mode, { type: "clear-hold", follow, canPinToBottom, suppressHold })
|
||||
this.state.mode = next.mode
|
||||
return this.result(next.effect)
|
||||
}
|
||||
|
||||
observeViewport(
|
||||
metrics: ScrollControllerMetrics,
|
||||
now: number,
|
||||
programmatic: boolean,
|
||||
canPinToBottom = false,
|
||||
forceEscapeFromUpScroll = false,
|
||||
): ScrollControllerResult {
|
||||
const previousOffset = this.state.lastObservedOffset
|
||||
const offset = metrics.offset
|
||||
const scrolledUp = offset < previousOffset - 1
|
||||
|
|
@ -247,7 +340,13 @@ export class VirtualScrollController {
|
|||
this.clearExpiredUserIntent(now)
|
||||
|
||||
const hasFreshIntent = now <= this.state.userIntentUntil
|
||||
if (scrolledUp && this.isAutoFollowing() && !atBottom && this.heldKey() === null && (!programmatic || hasFreshIntent)) {
|
||||
if (
|
||||
scrolledUp &&
|
||||
this.isAutoFollowing() &&
|
||||
(forceEscapeFromUpScroll || !atBottom) &&
|
||||
this.heldKey() === null &&
|
||||
(!programmatic || hasFreshIntent)
|
||||
) {
|
||||
return this.setFollow(false)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
import { Show, createEffect, createMemo, createSignal, type Accessor, type JSX, on, onCleanup } from "solid-js"
|
||||
import { Virtualizer, type VirtualizerHandle } from "virtua/solid"
|
||||
import { getHeldKey, isAutoFollowing, isAtBottom, VirtualScrollController, type FollowEffect, type FollowEvent, type FollowMode, type ScrollControllerMetrics, type ScrollControllerResult } from "./virtual-follow-behavior.ts"
|
||||
import { getFollowSnapshotState, getHeldKey, isAutoFollowing, isAtBottom, resolveAutoPinHoldElement, restoreFollowModeFromSnapshot, shouldSuspendAutoPinToBottomForHold, shouldTrackHeldAnchor, VirtualScrollController, type FollowEffect, type FollowEvent, type FollowMode, type HoldTargetElementResolver, type ScrollControllerMetrics, type ScrollControllerResult } from "./virtual-follow-behavior.ts"
|
||||
|
||||
const DEFAULT_SCROLL_SENTINEL_MARGIN_PX = 48
|
||||
const DEFAULT_HOLD_TARGET_TOP_THRESHOLD_PX = 8
|
||||
const DEFAULT_REJOIN_LAST_ITEM_COUNT = 2
|
||||
const USER_SCROLL_INTENT_WINDOW_MS = 600
|
||||
const BOTTOM_FOLLOW_INTENT_SETTLE_FRAMES = 4
|
||||
const BOTTOM_FOLLOW_INTENT_MAX_FRAMES = 60
|
||||
const SCROLL_INTENT_KEYS = new Set(["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " ", "Spacebar"])
|
||||
const INTERACTIVE_KEY_TARGET_SELECTOR = "button, a, input, textarea, select, [contenteditable='true'], [role='button'], [role='textbox']"
|
||||
|
||||
export interface VirtualFollowBottomIntent {
|
||||
token: string | number
|
||||
minItemCount?: number
|
||||
}
|
||||
|
||||
export interface VirtualFollowListApi {
|
||||
scrollToTop: (opts?: { immediate?: boolean }) => void
|
||||
scrollToBottom: (opts?: { immediate?: boolean; suppressAutoAnchor?: boolean; suppressHold?: boolean }) => void
|
||||
|
|
@ -32,6 +38,9 @@ export interface VirtualFollowScrollSnapshot {
|
|||
anchorKey?: string
|
||||
anchorOffset?: number
|
||||
atBottom: boolean
|
||||
followModeType?: FollowMode["type"]
|
||||
heldKey?: string
|
||||
holdAnchorSuspended?: boolean
|
||||
}
|
||||
|
||||
interface RestoreScrollSnapshotOptions {
|
||||
|
|
@ -65,6 +74,7 @@ export interface VirtualFollowListProps<T> {
|
|||
suspendMeasurements?: Accessor<boolean>
|
||||
streamingActive?: Accessor<boolean>
|
||||
isActive?: Accessor<boolean>
|
||||
autoPinHoldEnabled?: Accessor<boolean>
|
||||
|
||||
/**
|
||||
* When switching back to an inactive (cached) pane, the list historically
|
||||
|
|
@ -100,6 +110,12 @@ export interface VirtualFollowListProps<T> {
|
|||
*/
|
||||
followToken?: Accessor<string | number>
|
||||
|
||||
/**
|
||||
* Explicit submit intent that temporarily overrides stale hold/restore state
|
||||
* until the newly submitted exchange has mounted.
|
||||
*/
|
||||
forceBottomFollowIntent?: Accessor<VirtualFollowBottomIntent | null>
|
||||
|
||||
/**
|
||||
* Optional item key whose geometry can temporarily hold auto-follow when the
|
||||
* rendered item grows taller than the viewport and reaches the top edge.
|
||||
|
|
@ -108,9 +124,10 @@ export interface VirtualFollowListProps<T> {
|
|||
|
||||
/**
|
||||
* Optional resolver for the specific element inside an item wrapper that
|
||||
* should be measured for hold-target geometry.
|
||||
* should be measured for hold-target geometry. Return null when the item has
|
||||
* no eligible hold target; return undefined to fall back to the item wrapper.
|
||||
*/
|
||||
resolveAutoPinHoldElement?: (itemWrapper: HTMLDivElement, key: string) => HTMLElement | null | undefined
|
||||
resolveAutoPinHoldElement?: HoldTargetElementResolver
|
||||
|
||||
/**
|
||||
* Top-edge threshold for the hold target in pixels.
|
||||
|
|
@ -169,7 +186,9 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
const initialAutoScroll = () => (props.initialAutoScroll ? props.initialAutoScroll() : true)
|
||||
const externalSuspendAutoPinToBottom = () => (props.suspendAutoPinToBottom ? props.suspendAutoPinToBottom() : false)
|
||||
const streamingActive = () => props.streamingActive?.() ?? false
|
||||
const autoPinHoldEnabled = () => props.autoPinHoldEnabled?.() ?? true
|
||||
const holdTargetKey = () => (props.autoPinHoldTargetKey ? props.autoPinHoldTargetKey() : null)
|
||||
const forceBottomFollowIntent = () => props.forceBottomFollowIntent?.() ?? null
|
||||
const holdTargetTopThresholdPx = () => props.autoPinHoldTopThresholdPx ?? DEFAULT_HOLD_TARGET_TOP_THRESHOLD_PX
|
||||
|
||||
const scrollController = new VirtualScrollController(initialAutoScroll())
|
||||
|
|
@ -180,7 +199,11 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
const [activeKey, setActiveKey] = createSignal<string | null>(null)
|
||||
const activeHoldTargetKey = createMemo(() => getHeldKey(followMode()))
|
||||
const [didTriggerHoldForCurrentTarget, setDidTriggerHoldForCurrentTarget] = createSignal(false)
|
||||
const effectiveSuspendAutoPinToBottom = () => externalSuspendAutoPinToBottom() || activeHoldTargetKey() !== null
|
||||
const effectiveSuspendAutoPinToBottom = () => shouldSuspendAutoPinToBottomForHold({
|
||||
externalSuspend: externalSuspendAutoPinToBottom(),
|
||||
activeHoldTargetKey: activeHoldTargetKey(),
|
||||
eligibleHoldTargetKey: holdTargetKey(),
|
||||
})
|
||||
|
||||
const scrollButtonsCount = createMemo(() => (showScrollTopButton() ? 1 : 0) + (showScrollBottomButton() ? 1 : 0))
|
||||
const itemElements = new Map<string, HTMLDivElement>()
|
||||
|
|
@ -189,6 +212,7 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
let lastResetKey: string | number | undefined
|
||||
let suppressAutoScrollOnce = false
|
||||
let suppressHoldUntilTargetChanges = false
|
||||
let suspendHeldAnchorUntilBottom = false
|
||||
let lastItemsReference = props.items()
|
||||
let restoreToken = 0
|
||||
|
||||
|
|
@ -208,6 +232,15 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
let programmaticScrollUntil = 0
|
||||
let pendingBottomRepinAfterHold = false
|
||||
let pendingContentRenderedFrame: number | null = null
|
||||
let pendingBottomFollowIntentFrame: number | null = null
|
||||
let bottomFollowIntentToken: string | number | null = null
|
||||
let bottomFollowIntentMinItemCount = 0
|
||||
let bottomFollowIntentStaleHoldTargetKey: string | null = null
|
||||
let bottomFollowIntentSettleFrames = 0
|
||||
let bottomFollowIntentFramesRemaining = 0
|
||||
let heldAnchorKey: string | null = null
|
||||
let heldAnchorElement: HTMLElement | null = null
|
||||
let heldAnchorOffset: number | null = null
|
||||
|
||||
const state: VirtualFollowListState = {
|
||||
autoScroll,
|
||||
|
|
@ -218,6 +251,9 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
}
|
||||
|
||||
function markUserScrollIntent(direction?: "up" | "down" | null) {
|
||||
if (hasActiveBottomFollowIntent()) {
|
||||
clearBottomFollowIntent()
|
||||
}
|
||||
const now = performance.now()
|
||||
scrollController.setUserIntent(direction ?? null, now + USER_SCROLL_INTENT_WINDOW_MS)
|
||||
}
|
||||
|
|
@ -230,8 +266,123 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
return performance.now() <= programmaticScrollUntil
|
||||
}
|
||||
|
||||
function hasActiveBottomFollowIntent() {
|
||||
return bottomFollowIntentToken !== null
|
||||
}
|
||||
|
||||
function resetStaleHoldForBottomIntent() {
|
||||
restoreToken += 1
|
||||
scrollController.setRestoring(false)
|
||||
setDidTriggerHoldForCurrentTarget(false)
|
||||
suppressHoldUntilTargetChanges = true
|
||||
pendingBottomRepinAfterHold = false
|
||||
clearHeldAnchor()
|
||||
}
|
||||
|
||||
function clearBottomFollowIntent() {
|
||||
const staleTargetStillCurrent = bottomFollowIntentStaleHoldTargetKey !== null && holdTargetKey() === bottomFollowIntentStaleHoldTargetKey
|
||||
bottomFollowIntentToken = null
|
||||
bottomFollowIntentMinItemCount = 0
|
||||
bottomFollowIntentStaleHoldTargetKey = null
|
||||
bottomFollowIntentSettleFrames = 0
|
||||
bottomFollowIntentFramesRemaining = 0
|
||||
if (!staleTargetStillCurrent) {
|
||||
suppressHoldUntilTargetChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleBottomFollowIntentFrame() {
|
||||
if (!hasActiveBottomFollowIntent()) return
|
||||
if (pendingBottomFollowIntentFrame !== null) return
|
||||
if (typeof requestAnimationFrame !== "function") {
|
||||
runBottomFollowIntentFrame()
|
||||
return
|
||||
}
|
||||
pendingBottomFollowIntentFrame = requestAnimationFrame(() => runBottomFollowIntentFrame())
|
||||
}
|
||||
|
||||
function runBottomFollowIntentFrame() {
|
||||
pendingBottomFollowIntentFrame = null
|
||||
if (!hasActiveBottomFollowIntent()) return
|
||||
|
||||
resetStaleHoldForBottomIntent()
|
||||
dispatchFollowEvent({ type: "jump-bottom", immediate: true, explicit: true })
|
||||
|
||||
const minItemCountReached = props.items().length >= bottomFollowIntentMinItemCount
|
||||
if (minItemCountReached) {
|
||||
bottomFollowIntentSettleFrames -= 1
|
||||
} else {
|
||||
bottomFollowIntentSettleFrames = BOTTOM_FOLLOW_INTENT_SETTLE_FRAMES
|
||||
}
|
||||
bottomFollowIntentFramesRemaining -= 1
|
||||
|
||||
if ((minItemCountReached && bottomFollowIntentSettleFrames <= 0) || bottomFollowIntentFramesRemaining <= 0) {
|
||||
clearBottomFollowIntent()
|
||||
return
|
||||
}
|
||||
|
||||
scheduleBottomFollowIntentFrame()
|
||||
}
|
||||
|
||||
function startBottomFollowIntent(intent: VirtualFollowBottomIntent | null) {
|
||||
if (!intent) return
|
||||
if (!hasActiveBottomFollowIntent()) {
|
||||
bottomFollowIntentStaleHoldTargetKey = activeHoldTargetKey() ?? holdTargetKey()
|
||||
}
|
||||
bottomFollowIntentToken = intent.token
|
||||
bottomFollowIntentMinItemCount = Math.max(0, Math.floor(intent.minItemCount ?? 0))
|
||||
bottomFollowIntentSettleFrames = BOTTOM_FOLLOW_INTENT_SETTLE_FRAMES
|
||||
bottomFollowIntentFramesRemaining = BOTTOM_FOLLOW_INTENT_MAX_FRAMES
|
||||
resetStaleHoldForBottomIntent()
|
||||
runBottomFollowIntentFrame()
|
||||
}
|
||||
|
||||
function shouldManageHeldAnchor(key = activeHoldTargetKey(), targetKey = holdTargetKey()) {
|
||||
if (key === null || key !== activeHoldTargetKey()) return false
|
||||
return shouldTrackHeldAnchor({
|
||||
activeHoldTargetKey: key,
|
||||
eligibleHoldTargetKey: targetKey,
|
||||
suspendedByUser: suspendHeldAnchorUntilBottom,
|
||||
})
|
||||
}
|
||||
|
||||
function hasItemKey(key: string) {
|
||||
return props.items().some((item, index) => props.getKey(item, index) === key)
|
||||
}
|
||||
|
||||
function applyRestoredFollowSnapshot(snapshot: VirtualFollowScrollSnapshot) {
|
||||
const restoredMode = restoreFollowModeFromSnapshot({
|
||||
atBottom: snapshot.atBottom,
|
||||
followModeType: snapshot.followModeType,
|
||||
heldKey: snapshot.heldKey,
|
||||
hasHeldKeyItem: snapshot.heldKey ? hasItemKey(snapshot.heldKey) : false,
|
||||
})
|
||||
|
||||
suspendHeldAnchorUntilBottom = restoredMode.type === "holding" && Boolean(snapshot.holdAnchorSuspended)
|
||||
setFollowMode(restoredMode)
|
||||
scrollController.restoreMode(restoredMode)
|
||||
|
||||
if (restoredMode.type === "holding" && shouldManageHeldAnchor(restoredMode.key)) {
|
||||
captureHeldAnchor(restoredMode.key)
|
||||
return
|
||||
}
|
||||
|
||||
clearHeldAnchor()
|
||||
}
|
||||
|
||||
function syncControllerResult(result: ScrollControllerResult) {
|
||||
const previousMode = followMode()
|
||||
const previousHeldKey = previousMode.type === "holding" ? previousMode.key : null
|
||||
setFollowMode(result.state.mode)
|
||||
if (result.state.mode.type !== "holding") {
|
||||
suspendHeldAnchorUntilBottom = false
|
||||
clearHeldAnchor()
|
||||
} else if (previousHeldKey !== result.state.mode.key) {
|
||||
suspendHeldAnchorUntilBottom = false
|
||||
clearHeldAnchor()
|
||||
} else if (heldAnchorKey !== null && heldAnchorKey !== result.state.mode.key) {
|
||||
clearHeldAnchor()
|
||||
}
|
||||
applyFollowEffect(result.effect)
|
||||
}
|
||||
|
||||
|
|
@ -257,6 +408,7 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
performance.now(),
|
||||
hasProgrammaticScrollIntent(),
|
||||
canRejoinFollowFromDownScroll(metrics),
|
||||
autoPinHoldEnabled() && streamingActive(),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
|
@ -282,6 +434,9 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
case "hold-target-changed":
|
||||
result = scrollController.holdTargetChanged(event.key, event.canPinToBottom)
|
||||
break
|
||||
case "clear-hold":
|
||||
result = scrollController.clearHold(event.follow, event.canPinToBottom, event.suppressHold)
|
||||
break
|
||||
case "set-follow":
|
||||
result = scrollController.setFollow(event.enabled)
|
||||
break
|
||||
|
|
@ -408,14 +563,7 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
if (!streamingActive()) return false
|
||||
if (effectiveSuspendAutoPinToBottom()) return false
|
||||
if (activeHoldTargetKey() !== null) return false
|
||||
const items = props.items()
|
||||
if (items.length === 0) return false
|
||||
if (isAtBottom(metrics)) return true
|
||||
|
||||
const handle = virtuaHandle()
|
||||
if (!handle) return false
|
||||
const viewportEndIndex = handle.findItemIndex(metrics.offset + metrics.clientHeight - 1)
|
||||
return viewportEndIndex >= Math.max(items.length - DEFAULT_REJOIN_LAST_ITEM_COUNT, 0)
|
||||
return isAtBottom(metrics)
|
||||
}
|
||||
|
||||
function getSnapshotMetrics(element: HTMLDivElement, handle?: VirtualizerHandle) {
|
||||
|
|
@ -457,6 +605,14 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
if (!element) return undefined
|
||||
|
||||
const snapshot: VirtualFollowScrollSnapshot = getSnapshotMetrics(element, virtuaHandle())
|
||||
const currentMode = hasActiveBottomFollowIntent() ? ({ type: "following" } as const) : followMode()
|
||||
Object.assign(snapshot, getFollowSnapshotState(currentMode, suspendHeldAnchorUntilBottom))
|
||||
if (hasActiveBottomFollowIntent()) {
|
||||
snapshot.atBottom = true
|
||||
delete snapshot.anchorKey
|
||||
delete snapshot.anchorOffset
|
||||
return snapshot
|
||||
}
|
||||
if (!snapshot.atBottom) {
|
||||
const anchor = findTopVisibleAnchor(element)
|
||||
if (anchor) {
|
||||
|
|
@ -526,12 +682,19 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
return
|
||||
}
|
||||
|
||||
if (hasActiveBottomFollowIntent()) {
|
||||
scheduleBottomFollowIntentFrame()
|
||||
opts?.onApplied?.()
|
||||
return
|
||||
}
|
||||
|
||||
const token = ++restoreToken
|
||||
const isRestoreCurrent = () => token === restoreToken && Boolean(scrollElement())
|
||||
const behavior = opts?.behavior ?? "auto"
|
||||
scrollController.setRestoring(true)
|
||||
const finishRestore = () => {
|
||||
if (!isRestoreCurrent()) return
|
||||
applyRestoredFollowSnapshot(snapshot)
|
||||
scrollController.setRestoring(false)
|
||||
opts?.onApplied?.()
|
||||
}
|
||||
|
|
@ -596,9 +759,11 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
const element = scrollElement()
|
||||
if (!handle || !element) return
|
||||
|
||||
const previousSnapshot = scrollController.snapshot()
|
||||
const offset = getCurrentScrollOffset(element, handle)
|
||||
const now = performance.now()
|
||||
const programmatic = hasProgrammaticScrollIntent()
|
||||
const hasFreshUserIntent = now <= previousSnapshot.userIntentUntil
|
||||
const metrics = getDomMetrics(element, handle, offset)
|
||||
const atBottom = isAtBottom(metrics)
|
||||
const atTop = offset <= (props.scrollSentinelMarginPx ?? DEFAULT_SCROLL_SENTINEL_MARGIN_PX)
|
||||
|
|
@ -607,11 +772,29 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
setShowScrollBottomButton(hasItems && !atBottom)
|
||||
setShowScrollTopButton(hasItems && !atTop)
|
||||
|
||||
const result = scrollController.observeViewport(metrics, now, programmatic, canRejoinFollowFromDownScroll(metrics))
|
||||
const result = scrollController.observeViewport(
|
||||
metrics,
|
||||
now,
|
||||
programmatic,
|
||||
canRejoinFollowFromDownScroll(metrics),
|
||||
autoPinHoldEnabled() && streamingActive(),
|
||||
)
|
||||
const manualHoldScroll =
|
||||
previousSnapshot.mode.type === "holding" &&
|
||||
result.state.mode.type === "holding" &&
|
||||
hasFreshUserIntent &&
|
||||
Math.abs(offset - previousSnapshot.lastObservedOffset) > 1
|
||||
if (manualHoldScroll) {
|
||||
suspendHeldAnchorUntilBottom = true
|
||||
clearHeldAnchor()
|
||||
}
|
||||
if (result.state.mode.type !== followMode().type || result.effect.type !== "none") {
|
||||
suppressHoldUntilTargetChanges = false
|
||||
}
|
||||
syncControllerResult(result)
|
||||
if (result.state.mode.type === "holding" && shouldManageHeldAnchor(result.state.mode.key)) {
|
||||
captureHeldAnchor(result.state.mode.key)
|
||||
}
|
||||
}
|
||||
|
||||
function performScrollToBottom(immediate = true) {
|
||||
|
|
@ -707,12 +890,62 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
return props.getAnchorId ? props.getAnchorId(key) : key
|
||||
}
|
||||
|
||||
function resolveHoldTargetElement(key: string) {
|
||||
const itemWrapper = itemElements.get(key)
|
||||
return resolveAutoPinHoldElement(itemWrapper, key, props.resolveAutoPinHoldElement)
|
||||
}
|
||||
|
||||
function clearHeldAnchor() {
|
||||
heldAnchorKey = null
|
||||
heldAnchorElement = null
|
||||
heldAnchorOffset = null
|
||||
}
|
||||
|
||||
function captureHeldAnchor(key = activeHoldTargetKey(), target?: HTMLElement | null) {
|
||||
const element = scrollElement()
|
||||
if (!element || !key || !shouldManageHeldAnchor(key)) return false
|
||||
const resolvedTarget = target ?? (
|
||||
heldAnchorKey === key && heldAnchorElement && element.contains(heldAnchorElement)
|
||||
? heldAnchorElement
|
||||
: resolveHoldTargetElement(key)
|
||||
)
|
||||
if (!resolvedTarget || !element.contains(resolvedTarget)) return false
|
||||
const containerRect = element.getBoundingClientRect()
|
||||
const targetRect = resolvedTarget.getBoundingClientRect()
|
||||
heldAnchorKey = key
|
||||
heldAnchorElement = resolvedTarget
|
||||
heldAnchorOffset = targetRect.top - containerRect.top
|
||||
return true
|
||||
}
|
||||
|
||||
function restoreHeldAnchor() {
|
||||
const key = activeHoldTargetKey()
|
||||
const element = scrollElement()
|
||||
if (!element || !key || heldAnchorKey !== key || heldAnchorOffset === null) return false
|
||||
const target = heldAnchorElement
|
||||
if (!target || !element.contains(target)) {
|
||||
clearHeldAnchor()
|
||||
return false
|
||||
}
|
||||
|
||||
const containerRect = element.getBoundingClientRect()
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
const currentOffset = targetRect.top - containerRect.top
|
||||
const delta = currentOffset - heldAnchorOffset
|
||||
if (Math.abs(delta) > 1) {
|
||||
scrollToOffset((virtuaHandle()?.scrollOffset ?? element.scrollTop) + delta, false)
|
||||
}
|
||||
captureHeldAnchor(key, target)
|
||||
return true
|
||||
}
|
||||
|
||||
function alignHoldTarget(key: string) {
|
||||
if (activeHoldTargetKey() !== key) return
|
||||
if (!shouldManageHeldAnchor(key)) return
|
||||
const element = scrollElement()
|
||||
if (!element) return
|
||||
const itemWrapper = itemElements.get(key)
|
||||
if (!itemWrapper) return
|
||||
const target = props.resolveAutoPinHoldElement?.(itemWrapper, key) ?? itemWrapper
|
||||
const target = resolveHoldTargetElement(key)
|
||||
if (!target) return
|
||||
const containerRect = element.getBoundingClientRect()
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
const relativeTop = targetRect.top - containerRect.top
|
||||
|
|
@ -720,24 +953,33 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
if (Math.abs(alignDelta) > 1) {
|
||||
scrollToOffset((virtuaHandle()?.scrollOffset ?? element.scrollTop) + alignDelta, false)
|
||||
}
|
||||
captureHeldAnchor(key, target)
|
||||
requestAnimationFrame(() => captureHeldAnchor(key, target))
|
||||
}
|
||||
|
||||
function updateAutoPinHold() {
|
||||
const element = scrollElement()
|
||||
if (!element) return
|
||||
if (hasActiveBottomFollowIntent()) return
|
||||
|
||||
const targetKey = holdTargetKey()
|
||||
const heldKey = activeHoldTargetKey()
|
||||
|
||||
if (heldKey !== null) {
|
||||
if (targetKey !== heldKey) {
|
||||
suspendHeldAnchorUntilBottom = true
|
||||
clearHeldAnchor()
|
||||
dispatchFollowEvent({ type: "hold-target-changed", key: targetKey, canPinToBottom: !externalSuspendAutoPinToBottom() })
|
||||
}
|
||||
if (shouldManageHeldAnchor(heldKey, targetKey) && (heldAnchorKey !== heldKey || heldAnchorOffset === null)) {
|
||||
captureHeldAnchor(heldKey)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!streamingActive()) return
|
||||
if (!autoPinHoldEnabled()) return
|
||||
if (!autoScroll()) return
|
||||
if (externalSuspendAutoPinToBottom()) return
|
||||
if (!targetKey) return
|
||||
|
|
@ -745,15 +987,15 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
if (suppressHoldUntilTargetChanges) return
|
||||
|
||||
const itemWrapper = itemElements.get(targetKey)
|
||||
if (!itemWrapper) return
|
||||
const target = props.resolveAutoPinHoldElement?.(itemWrapper, targetKey) ?? itemWrapper
|
||||
const target = resolveAutoPinHoldElement(itemWrapper, targetKey, props.resolveAutoPinHoldElement)
|
||||
if (!target) return
|
||||
|
||||
const containerRect = element.getBoundingClientRect()
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
const relativeTop = targetRect.top - containerRect.top
|
||||
const exceedsViewport = targetRect.height > element.clientHeight
|
||||
|
||||
if (exceedsViewport && relativeTop < 0) {
|
||||
if (exceedsViewport && relativeTop <= holdTargetTopThresholdPx()) {
|
||||
dispatchFollowEvent({ type: "hold-candidate", key: targetKey, shouldHold: true })
|
||||
setDidTriggerHoldForCurrentTarget(true)
|
||||
}
|
||||
|
|
@ -762,6 +1004,12 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
function flushContentRendered() {
|
||||
pendingContentRenderedFrame = null
|
||||
|
||||
if (hasActiveBottomFollowIntent()) {
|
||||
scheduleBottomFollowIntentFrame()
|
||||
updateScrollButtons()
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldHonorPrePinEscape() && escapeFollowIfDomMovedUp()) {
|
||||
updateScrollButtons()
|
||||
return
|
||||
|
|
@ -770,6 +1018,9 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
updateAutoPinHold()
|
||||
if (activeHoldTargetKey() !== null) {
|
||||
if (autoScroll() && streamingActive()) pendingBottomRepinAfterHold = true
|
||||
if (shouldManageHeldAnchor()) {
|
||||
restoreHeldAnchor()
|
||||
}
|
||||
updateScrollButtons()
|
||||
return
|
||||
}
|
||||
|
|
@ -817,18 +1068,22 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
itemElements.clear()
|
||||
setDidTriggerHoldForCurrentTarget(false)
|
||||
suppressHoldUntilTargetChanges = false
|
||||
suspendHeldAnchorUntilBottom = false
|
||||
pendingBottomRepinAfterHold = false
|
||||
clearHeldAnchor()
|
||||
}))
|
||||
|
||||
createEffect(on(holdTargetKey, (nextTargetKey, prevTargetKey) => {
|
||||
if (nextTargetKey !== prevTargetKey && didTriggerHoldForCurrentTarget()) {
|
||||
setDidTriggerHoldForCurrentTarget(false)
|
||||
}
|
||||
if (nextTargetKey !== prevTargetKey) {
|
||||
if (nextTargetKey !== prevTargetKey && !hasActiveBottomFollowIntent()) {
|
||||
suppressHoldUntilTargetChanges = false
|
||||
}
|
||||
if (activeHoldTargetKey() === null) return
|
||||
if (nextTargetKey === activeHoldTargetKey()) return
|
||||
suspendHeldAnchorUntilBottom = true
|
||||
clearHeldAnchor()
|
||||
dispatchFollowEvent({ type: "hold-target-changed", key: nextTargetKey, canPinToBottom: !externalSuspendAutoPinToBottom() })
|
||||
if (pendingBottomRepinAfterHold) {
|
||||
pendingBottomRepinAfterHold = false
|
||||
|
|
@ -836,6 +1091,30 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
}
|
||||
}, { defer: true }))
|
||||
|
||||
createEffect(on(autoPinHoldEnabled, (enabled) => {
|
||||
if (enabled) return
|
||||
setDidTriggerHoldForCurrentTarget(false)
|
||||
suppressHoldUntilTargetChanges = false
|
||||
suspendHeldAnchorUntilBottom = false
|
||||
pendingBottomRepinAfterHold = false
|
||||
if (activeHoldTargetKey() !== null) {
|
||||
dispatchFollowEvent({
|
||||
type: "clear-hold",
|
||||
follow: true,
|
||||
canPinToBottom: !externalSuspendAutoPinToBottom(),
|
||||
suppressHold: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
clearHeldAnchor()
|
||||
}, { defer: true }))
|
||||
|
||||
createEffect(on(forceBottomFollowIntent, (intent) => {
|
||||
if (!intent) return
|
||||
if (intent.token === bottomFollowIntentToken) return
|
||||
startBottomFollowIntent(intent)
|
||||
}, { defer: true }))
|
||||
|
||||
// Handle autoScroll (Follow) on items change
|
||||
createEffect(on(() => props.items().length, (len, prevLen) => {
|
||||
if (pendingInitialScroll && isActive() && len > 0) {
|
||||
|
|
@ -847,6 +1126,11 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
return
|
||||
}
|
||||
if (len > (prevLen ?? 0) && autoScroll() && !effectiveSuspendAutoPinToBottom() && !suppressAutoScrollOnce) {
|
||||
if (hasActiveBottomFollowIntent()) {
|
||||
scheduleBottomFollowIntentFrame()
|
||||
suppressAutoScrollOnce = false
|
||||
return
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
dispatchFollowEvent({ type: "content-grew", canPinToBottom: autoScroll() && !effectiveSuspendAutoPinToBottom() })
|
||||
})
|
||||
|
|
@ -888,6 +1172,10 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||
cancelAnimationFrame(pendingContentRenderedFrame)
|
||||
pendingContentRenderedFrame = null
|
||||
}
|
||||
if (pendingBottomFollowIntentFrame !== null && typeof cancelAnimationFrame === "function") {
|
||||
cancelAnimationFrame(pendingBottomFollowIntentFrame)
|
||||
pendingBottomFollowIntentFrame = null
|
||||
}
|
||||
detachScrollIntentListeners?.()
|
||||
detachScrollIntentListeners = undefined
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const appMessages = {
|
|||
"app.stopInstance.title": "Instanz stoppen",
|
||||
"app.stopInstance.confirmLabel": "Stoppen",
|
||||
"app.stopInstance.cancelLabel": "Weiterlaufen lassen",
|
||||
"app.stopInstance.toast.error": "Arbeitsbereich konnte nicht gestoppt werden.",
|
||||
|
||||
"emptyState.logoAlt": "CodeNomad Logo",
|
||||
"emptyState.brandTitle": "CodeNomad",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export const instanceMessages = {
|
|||
"instanceInfo.labels.folder": "Ordner",
|
||||
"instanceInfo.labels.project": "Projekt",
|
||||
"instanceInfo.labels.versionControl": "Versionskontrolle",
|
||||
"instanceInfo.labels.opencodeVersion": "OpenCode Version",
|
||||
"instanceInfo.labels.opencodeVersion": "OpenCode-Version",
|
||||
"instanceInfo.labels.binaryPath": "Binärpfad",
|
||||
"instanceInfo.labels.environmentVariables": "Umgebungsvariablen ({count})",
|
||||
"instanceInfo.loading": "Wird geladen...",
|
||||
|
|
@ -130,8 +130,8 @@ export const instanceMessages = {
|
|||
"instanceShell.gitChanges.actions.stage": "Datei stagen",
|
||||
"instanceShell.gitChanges.actions.unstage": "Datei unstagen",
|
||||
"instanceShell.gitChanges.commit.placeholder": "Commit-Nachricht eingeben",
|
||||
"instanceShell.gitChanges.commit.submit": "Commit",
|
||||
"instanceShell.gitChanges.commit.submitting": "Committing...",
|
||||
"instanceShell.gitChanges.commit.submit": "Commit erstellen",
|
||||
"instanceShell.gitChanges.commit.submitting": "Commit wird erstellt...",
|
||||
"instanceShell.gitChanges.commit.success": "Commit erfolgreich erstellt",
|
||||
"instanceShell.gitChanges.commit.error": "Commit konnte nicht erstellt werden",
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const messagingMessages = {
|
|||
"messageSection.loadError.reload": "Nachrichten neu laden",
|
||||
"messageSection.scroll.toFirstAriaLabel": "Zur ersten Nachricht scrollen",
|
||||
"messageSection.scroll.toLatestAriaLabel": "Zur neuesten Nachricht scrollen",
|
||||
"messageSection.scroll.showControlsAriaLabel": "Scroll-Steuerung anzeigen",
|
||||
"messageSection.scroll.enableHoldAriaLabel": "Halten für lange Assistentenantworten aktivieren",
|
||||
"messageSection.scroll.disableHoldAriaLabel": "Halten für lange Assistentenantworten deaktivieren",
|
||||
"messageSection.quote.addAsQuote": "Als Zitat hinzufügen",
|
||||
|
|
@ -78,6 +79,15 @@ export const messagingMessages = {
|
|||
"messageBlock.reasoning.indicator.hide": "Verbergen",
|
||||
"messageBlock.reasoning.indicator.view": "Ansehen",
|
||||
"messageBlock.reasoning.detailsAriaLabel": "Denk-Details",
|
||||
"messageBlock.reasoning.thoughtsFallback": "Gedanken",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.one": "{count} Sekunde nachgedacht",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.other": "{count} Sekunden nachgedacht",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.one": "{count} Minute nachgedacht",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.other": "{count} Minuten nachgedacht",
|
||||
"messageBlock.reasoning.thoughtFor.hours.one": "{count} Stunde nachgedacht",
|
||||
"messageBlock.reasoning.thoughtFor.hours.other": "{count} Stunden nachgedacht",
|
||||
"messageBlock.reasoning.copyTitle": "Gedanken kopieren",
|
||||
"messageBlock.reasoning.copyAriaLabel": "Gedanken kopieren",
|
||||
|
||||
"codeBlockInline.actions.copy": "Kopieren",
|
||||
"codeBlockInline.actions.copied": "Kopiert!",
|
||||
|
|
@ -106,6 +116,14 @@ export const messagingMessages = {
|
|||
"messageItem.actions.deleteMessageFailedMessage": "Nachricht konnte nicht gelöscht werden",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "Nachricht zum Löschen auswählen",
|
||||
"messageItem.selection.selectForDeletion": "Nachricht zum Löschen auswählen",
|
||||
"messageItem.selection.deselectForDeletion": "Nachricht zum Löschen abwählen",
|
||||
"messageItem.duration.workedFor.seconds.one": "Hat {count} Sekunde gearbeitet",
|
||||
"messageItem.duration.workedFor.seconds.other": "Hat {count} Sekunden gearbeitet",
|
||||
"messageItem.duration.workedFor.minutes.one": "Hat {count} Minute gearbeitet",
|
||||
"messageItem.duration.workedFor.minutes.other": "Hat {count} Minuten gearbeitet",
|
||||
"messageItem.duration.workedFor.hours.one": "Hat {count} Stunde gearbeitet",
|
||||
"messageItem.duration.workedFor.hours.other": "Hat {count} Stunden gearbeitet",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "Ausgewählte Elemente ({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "Ausgewählte Elemente löschen",
|
||||
|
|
@ -129,6 +147,10 @@ export const messagingMessages = {
|
|||
"messagePart.actions.deleteTitle": "Dieses Element löschen",
|
||||
"messagePart.actions.deleteFailedTitle": "Löschen fehlgeschlagen",
|
||||
"messagePart.actions.deleteFailedMessage": "Element konnte nicht gelöscht werden",
|
||||
"messagePart.pastedText.summary": "Eingefügter Text",
|
||||
"messagePart.pastedText.lines.one": "{count} Zeile",
|
||||
"messagePart.pastedText.lines.other": "{count} Zeilen",
|
||||
"messagePart.pastedText.copyAriaLabel": "Eingefügten Textblock kopieren",
|
||||
"messageItem.attachment.defaultName": "Anhang",
|
||||
"messageItem.attachment.downloadAriaLabel": "{name} herunterladen",
|
||||
"messageItem.agentMeta.agentLabel": "Agent: {agent}",
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export const settingsMessages = {
|
|||
"settings.opencode.logLevel.option.debug": "Debug",
|
||||
"settings.opencode.logLevel.option.info": "Info",
|
||||
"settings.opencode.logLevel.option.warn": "Warn",
|
||||
"settings.opencode.logLevel.option.error": "Error",
|
||||
"settings.opencode.logLevel.option.error": "Fehler",
|
||||
"settings.configFiles.title": "Konfigurationsdateien",
|
||||
"settings.configFiles.subtitle": "Bearbeiten Sie freigegebene globale Konfigurationsdateien auf diesem Server.",
|
||||
"settings.configFiles.selector.label": "Datei",
|
||||
|
|
@ -210,6 +210,23 @@ export const settingsMessages = {
|
|||
|
||||
"settings.appearance.behavior.title": "Interaktion",
|
||||
"settings.appearance.behavior.subtitle": "Standardwerte für Nachrichten, Diffs und Eingaben.",
|
||||
"settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets",
|
||||
"settings.behavior.expansionPreset.minimal.title": "Minimal",
|
||||
"settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.",
|
||||
"settings.behavior.expansionPreset.balanced.title": "Balanced",
|
||||
"settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.",
|
||||
"settings.behavior.expansionPreset.detailed.title": "Detailed",
|
||||
"settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.",
|
||||
"settings.behavior.expansionPreset.everything.title": "Everything",
|
||||
"settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.",
|
||||
"settings.behavior.expansionPreset.custom.title": "Custom",
|
||||
"settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.",
|
||||
"settings.behavior.expansionDefaults.title": "Expansion defaults",
|
||||
"settings.behavior.expansionDefaults.itemColumn": "Item",
|
||||
"settings.behavior.expansionDefaults.stateColumn": "Default state",
|
||||
"settings.behavior.expansionDefaults.thinking": "thinking",
|
||||
"settings.behavior.expansionDefaults.otherTools": "Other tools",
|
||||
"settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}",
|
||||
"settings.behavior.keyboardHints.title": "Tastenkombinations-Hinweise",
|
||||
"settings.behavior.keyboardHints.subtitle": "Tastenkombinations-Hinweise in der UI anzeigen.",
|
||||
"settings.behavior.thinking.title": "Denkabschnitte",
|
||||
|
|
@ -236,6 +253,8 @@ export const settingsMessages = {
|
|||
"settings.behavior.autoCleanup.subtitle": "Leere Sitzungen beim Erstellen neuer Sitzungen automatisch bereinigen.",
|
||||
"settings.behavior.keepUnseenSubagentIdle.title": "Statusanzeigen von Unteragenten beibehalten",
|
||||
"settings.behavior.keepUnseenSubagentIdle.subtitle": "Statusanzeigen von Unteragenten sichtbar lassen, bis sie angesehen wurden.",
|
||||
"settings.behavior.tauriNativeEventTransport.title": "Nativer Tauri-Ereignistransport",
|
||||
"settings.behavior.tauriNativeEventTransport.subtitle": "Den Rust-nativen Desktop-Ereignistransport in Tauri verwenden. Deaktivieren, um auf den Browser-EventSource-Pfad zurückzufallen.",
|
||||
"settings.behavior.promptVoiceInput.title": "Prompt-Spracheingabe",
|
||||
"settings.behavior.promptVoiceInput.subtitle": "Mikrofon-Steuerung für Sprache-zu-Text anzeigen, wenn Sprache konfiguriert ist.",
|
||||
"settings.behavior.promptSubmit.title": "Enter zum Absenden",
|
||||
|
|
@ -265,14 +284,14 @@ export const settingsMessages = {
|
|||
"settings.speech.ttsVoice.title": "Standardstimme",
|
||||
"settings.speech.ttsVoice.subtitle": "Standardstimme für Text-zu-Sprache.",
|
||||
"settings.speech.playbackMode.title": "Wiedergabemodus",
|
||||
"settings.speech.playbackMode.subtitle": "Wählen Sie, ob die Wiedergabe sofort (Streaming) oder nach der Generierung (Buffered) startet.",
|
||||
"settings.speech.playbackMode.subtitle": "Wählen Sie, ob die Wiedergabe sofort (Streaming) oder nach der Generierung (gepuffert) startet.",
|
||||
"settings.speech.playbackMode.streaming": "Streaming",
|
||||
"settings.speech.playbackMode.buffered": "Buffered",
|
||||
"settings.speech.playbackMode.buffered": "Gepuffert",
|
||||
"settings.speech.ttsFormat.title": "Ausgabeformat",
|
||||
"settings.speech.ttsFormat.subtitle": "Wählen Sie das Audioformat für die synthetisierte Sprache.",
|
||||
"settings.speech.help": "Die Spracheingabe erscheint, wenn die Transkription konfiguriert ist. Die Wiedergabe verwendet den gewählten TTS-Modus.",
|
||||
"settings.speech.compatibility.streamingUnavailable": "Ihr Anbieter unterstützt kein Streaming-TTS. Schalten Sie auf 'Buffered' um.",
|
||||
"settings.speech.compatibility.browserStreamingUnavailable": "Ihr Browser unterstützt kein Streaming für dieses Format. Wählen Sie 'Buffered' oder ein anderes Format.",
|
||||
"settings.speech.compatibility.streamingUnavailable": "Ihr Anbieter unterstützt kein Streaming-TTS. Schalten Sie auf 'Gepuffert' um.",
|
||||
"settings.speech.compatibility.browserStreamingUnavailable": "Ihr Browser unterstützt kein Streaming für dieses Format. Wählen Sie 'Gepuffert' oder ein anderes Format.",
|
||||
"settings.speech.compatibility.runtimeNote": "Alle Formate bleiben im Streaming-Modus auswählbar. Manche Browser-Anbieter-Kombinationen können bei der Wiedergabe dennoch fehlschlagen.",
|
||||
"settings.speech.testPlayback.action": "Wiedergabe testen",
|
||||
"settings.speech.testPlayback.generating": "Beispiel wird generiert",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ export const toolCallMessages = {
|
|||
|
||||
"toolCall.header.copyTitle": "Tool-Aufruf-Titel kopieren",
|
||||
"toolCall.header.copyAriaLabel": "Tool-Aufruf-Titel kopieren",
|
||||
"toolCall.header.copyOutputTitle": "Tool-Ausgabe kopieren",
|
||||
"toolCall.header.copyOutputAriaLabel": "Tool-Ausgabe kopieren",
|
||||
|
||||
"toolCall.header.showInputTitle": "Tool-Argumente anzeigen",
|
||||
"toolCall.header.showInputAriaLabel": "Tool-Argumente anzeigen",
|
||||
|
|
@ -12,6 +14,10 @@ export const toolCallMessages = {
|
|||
|
||||
"toolCall.io.input": "Tool-Eingabe",
|
||||
"toolCall.io.output": "Tool-Ausgabe",
|
||||
"toolCall.io.copyInputTitle": "Tool-Eingabe kopieren",
|
||||
"toolCall.io.copyInputAriaLabel": "Tool-Eingabe kopieren",
|
||||
"toolCall.io.copyOutputTitle": "Tool-Ausgabe kopieren",
|
||||
"toolCall.io.copyOutputAriaLabel": "Tool-Ausgabe kopieren",
|
||||
|
||||
"toolCall.diff.label": "Diff",
|
||||
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||
|
|
@ -53,7 +59,7 @@ export const toolCallMessages = {
|
|||
"toolCall.renderer.read.detail.offset": "Offset: {offset}",
|
||||
"toolCall.renderer.read.detail.limit": "Limit: {limit}",
|
||||
|
||||
"toolCall.renderer.todo.empty": "Noch keine Planelemente.",
|
||||
"toolCall.renderer.todo.empty": "Keine Elemente geplant",
|
||||
"toolCall.renderer.todo.status.pending": "Ausstehend",
|
||||
"toolCall.renderer.todo.status.inProgress": "In Arbeit",
|
||||
"toolCall.renderer.todo.status.completed": "Abgeschlossen",
|
||||
|
|
@ -71,13 +77,9 @@ export const toolCallMessages = {
|
|||
"toolCall.permission.actions.allowOnce": "Einmalig erlauben",
|
||||
"toolCall.permission.actions.alwaysAllow": "Immer erlauben",
|
||||
"toolCall.permission.actions.deny": "Ablehnen",
|
||||
"toolCall.permission.actions.confirmDeny": "Ablehnung bestätigen",
|
||||
"toolCall.permission.actions.cancel": "Abbrechen",
|
||||
"toolCall.permission.shortcuts.allowOnce": "Einmalig erlauben",
|
||||
"toolCall.permission.shortcuts.alwaysAllow": "Immer erlauben",
|
||||
"toolCall.permission.rejectReason.label": "Warum diese Berechtigung ablehnen?",
|
||||
"toolCall.permission.rejectReason.placeholder": "Sagen Sie CodeNomad, was anders gemacht werden soll...",
|
||||
"toolCall.permission.rejectReason.hint": "Der Agent sieht dieses Feedback und kann seine nächste Aktion anpassen.",
|
||||
"toolCall.permission.rejectReason.placeholder": "Optionales Feedback beim Ablehnen, z. B. einen sichereren Befehl verwenden...",
|
||||
"toolCall.permission.errors.unableToUpdate": "Berechtigung konnte nicht aktualisiert werden",
|
||||
|
||||
"permissionApproval.title": "Anfragen",
|
||||
|
|
@ -94,11 +96,7 @@ export const toolCallMessages = {
|
|||
"permissionApproval.actions.allowOnce": "Einmalig erlauben",
|
||||
"permissionApproval.actions.alwaysAllow": "Immer erlauben",
|
||||
"permissionApproval.actions.deny": "Ablehnen",
|
||||
"permissionApproval.actions.confirmDeny": "Ablehnung bestätigen",
|
||||
"permissionApproval.actions.cancel": "Abbrechen",
|
||||
"permissionApproval.rejectReason.label": "Warum diese Berechtigung ablehnen?",
|
||||
"permissionApproval.rejectReason.placeholder": "Sagen Sie CodeNomad, was anders gemacht werden soll...",
|
||||
"permissionApproval.rejectReason.hint": "Der Agent sieht dieses Feedback und kann seine nächste Aktion anpassen.",
|
||||
"permissionApproval.rejectReason.placeholder": "Optionales Feedback beim Ablehnen, z. B. einen sichereren Befehl verwenden...",
|
||||
"permissionApproval.fallbackHint": "Sitzung für weitere Informationen laden.",
|
||||
"permissionApproval.errors.unableToUpdatePermission": "Berechtigung konnte nicht aktualisiert werden",
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const appMessages = {
|
|||
"app.stopInstance.title": "Stop instance",
|
||||
"app.stopInstance.confirmLabel": "Stop",
|
||||
"app.stopInstance.cancelLabel": "Keep running",
|
||||
"app.stopInstance.toast.error": "Failed to stop workspace.",
|
||||
|
||||
"emptyState.logoAlt": "CodeNomad logo",
|
||||
"emptyState.brandTitle": "CodeNomad",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const messagingMessages = {
|
|||
"messageSection.loadError.reload": "Reload messages",
|
||||
"messageSection.scroll.toFirstAriaLabel": "Scroll to first message",
|
||||
"messageSection.scroll.toLatestAriaLabel": "Scroll to latest message",
|
||||
"messageSection.scroll.showControlsAriaLabel": "Show scroll controls",
|
||||
"messageSection.scroll.enableHoldAriaLabel": "Enable hold for long assistant replies",
|
||||
"messageSection.scroll.disableHoldAriaLabel": "Disable hold for long assistant replies",
|
||||
"messageSection.quote.addAsQuote": "Add as quote",
|
||||
|
|
@ -78,6 +79,15 @@ export const messagingMessages = {
|
|||
"messageBlock.reasoning.indicator.hide": "Hide",
|
||||
"messageBlock.reasoning.indicator.view": "View",
|
||||
"messageBlock.reasoning.detailsAriaLabel": "Reasoning details",
|
||||
"messageBlock.reasoning.thoughtsFallback": "Thoughts",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.one": "Thought for {count} second",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.other": "Thought for {count} seconds",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.one": "Thought for {count} minute",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.other": "Thought for {count} minutes",
|
||||
"messageBlock.reasoning.thoughtFor.hours.one": "Thought for {count} hour",
|
||||
"messageBlock.reasoning.thoughtFor.hours.other": "Thought for {count} hours",
|
||||
"messageBlock.reasoning.copyTitle": "Copy thoughts",
|
||||
"messageBlock.reasoning.copyAriaLabel": "Copy thoughts",
|
||||
|
||||
"codeBlockInline.actions.copy": "Copy",
|
||||
"codeBlockInline.actions.copied": "Copied!",
|
||||
|
|
@ -106,6 +116,14 @@ export const messagingMessages = {
|
|||
"messageItem.actions.deleteMessageFailedMessage": "Failed to delete message",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "Select message for deletion",
|
||||
"messageItem.selection.selectForDeletion": "Select message for deletion",
|
||||
"messageItem.selection.deselectForDeletion": "Deselect message for deletion",
|
||||
"messageItem.duration.workedFor.seconds.one": "Worked for {count} second",
|
||||
"messageItem.duration.workedFor.seconds.other": "Worked for {count} seconds",
|
||||
"messageItem.duration.workedFor.minutes.one": "Worked for {count} minute",
|
||||
"messageItem.duration.workedFor.minutes.other": "Worked for {count} minutes",
|
||||
"messageItem.duration.workedFor.hours.one": "Worked for {count} hour",
|
||||
"messageItem.duration.workedFor.hours.other": "Worked for {count} hours",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "Selected items ({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "Delete selected items",
|
||||
|
|
|
|||
|
|
@ -210,6 +210,23 @@ export const settingsMessages = {
|
|||
|
||||
"settings.appearance.behavior.title": "Interaction",
|
||||
"settings.appearance.behavior.subtitle": "Message, diff, and input defaults.",
|
||||
"settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets",
|
||||
"settings.behavior.expansionPreset.minimal.title": "Minimal",
|
||||
"settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.",
|
||||
"settings.behavior.expansionPreset.balanced.title": "Balanced",
|
||||
"settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.",
|
||||
"settings.behavior.expansionPreset.detailed.title": "Detailed",
|
||||
"settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.",
|
||||
"settings.behavior.expansionPreset.everything.title": "Everything",
|
||||
"settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.",
|
||||
"settings.behavior.expansionPreset.custom.title": "Custom",
|
||||
"settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.",
|
||||
"settings.behavior.expansionDefaults.title": "Expansion defaults",
|
||||
"settings.behavior.expansionDefaults.itemColumn": "Item",
|
||||
"settings.behavior.expansionDefaults.stateColumn": "Default state",
|
||||
"settings.behavior.expansionDefaults.thinking": "thinking",
|
||||
"settings.behavior.expansionDefaults.otherTools": "Other tools",
|
||||
"settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}",
|
||||
"settings.behavior.keyboardHints.title": "Keyboard shortcut hints",
|
||||
"settings.behavior.keyboardHints.subtitle": "Show keyboard shortcut hints across the UI.",
|
||||
"settings.behavior.thinking.title": "Thinking sections",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ export const toolCallMessages = {
|
|||
|
||||
"toolCall.header.copyTitle": "Copy tool call title",
|
||||
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
||||
"toolCall.header.copyOutputTitle": "Copy tool output",
|
||||
"toolCall.header.copyOutputAriaLabel": "Copy tool output",
|
||||
|
||||
"toolCall.header.showInputTitle": "Show Tool Arguments",
|
||||
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
|
||||
|
|
@ -12,6 +14,10 @@ export const toolCallMessages = {
|
|||
|
||||
"toolCall.io.input": "Tool Input",
|
||||
"toolCall.io.output": "Tool Output",
|
||||
"toolCall.io.copyInputTitle": "Copy tool input",
|
||||
"toolCall.io.copyInputAriaLabel": "Copy tool input",
|
||||
"toolCall.io.copyOutputTitle": "Copy tool output",
|
||||
"toolCall.io.copyOutputAriaLabel": "Copy tool output",
|
||||
|
||||
"toolCall.diff.label": "Diff",
|
||||
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||
|
|
@ -53,7 +59,7 @@ export const toolCallMessages = {
|
|||
"toolCall.renderer.read.detail.offset": "Offset: {offset}",
|
||||
"toolCall.renderer.read.detail.limit": "Limit: {limit}",
|
||||
|
||||
"toolCall.renderer.todo.empty": "No plan items yet.",
|
||||
"toolCall.renderer.todo.empty": "No items planned",
|
||||
"toolCall.renderer.todo.status.pending": "Pending",
|
||||
"toolCall.renderer.todo.status.inProgress": "In progress",
|
||||
"toolCall.renderer.todo.status.completed": "Completed",
|
||||
|
|
@ -71,13 +77,9 @@ export const toolCallMessages = {
|
|||
"toolCall.permission.actions.allowOnce": "Allow Once",
|
||||
"toolCall.permission.actions.alwaysAllow": "Always Allow",
|
||||
"toolCall.permission.actions.deny": "Deny",
|
||||
"toolCall.permission.actions.confirmDeny": "Confirm Deny",
|
||||
"toolCall.permission.actions.cancel": "Cancel",
|
||||
"toolCall.permission.shortcuts.allowOnce": "Allow once",
|
||||
"toolCall.permission.shortcuts.alwaysAllow": "Always allow",
|
||||
"toolCall.permission.rejectReason.label": "Why deny this permission?",
|
||||
"toolCall.permission.rejectReason.placeholder": "Tell CodeNomad what to do differently...",
|
||||
"toolCall.permission.rejectReason.hint": "The agent will see this feedback and can adjust its next action.",
|
||||
"toolCall.permission.rejectReason.placeholder": "Optional feedback for Deny, e.g. use a safer command...",
|
||||
"toolCall.permission.errors.unableToUpdate": "Unable to update permission",
|
||||
|
||||
"permissionApproval.title": "Requests",
|
||||
|
|
@ -94,11 +96,7 @@ export const toolCallMessages = {
|
|||
"permissionApproval.actions.allowOnce": "Allow Once",
|
||||
"permissionApproval.actions.alwaysAllow": "Always Allow",
|
||||
"permissionApproval.actions.deny": "Deny",
|
||||
"permissionApproval.actions.confirmDeny": "Confirm Deny",
|
||||
"permissionApproval.actions.cancel": "Cancel",
|
||||
"permissionApproval.rejectReason.label": "Why deny this permission?",
|
||||
"permissionApproval.rejectReason.placeholder": "Tell CodeNomad what to do differently...",
|
||||
"permissionApproval.rejectReason.hint": "The agent will see this feedback and can adjust its next action.",
|
||||
"permissionApproval.rejectReason.placeholder": "Optional feedback for Deny, e.g. use a safer command...",
|
||||
"permissionApproval.fallbackHint": "Load session for more information.",
|
||||
"permissionApproval.errors.unableToUpdatePermission": "Unable to update permission",
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const appMessages = {
|
|||
"app.stopInstance.title": "Detener instancia",
|
||||
"app.stopInstance.confirmLabel": "Detener",
|
||||
"app.stopInstance.cancelLabel": "Seguir ejecutándose",
|
||||
"app.stopInstance.toast.error": "No se pudo detener el workspace.",
|
||||
|
||||
"emptyState.logoAlt": "Logo de CodeNomad",
|
||||
"emptyState.brandTitle": "CodeNomad",
|
||||
|
|
@ -33,4 +34,10 @@ export const appMessages = {
|
|||
"releases.devUpdateAvailable.title": "Compilación dev disponible",
|
||||
"releases.devUpdateAvailable.message": "Hay una nueva compilación dev disponible: {version}.",
|
||||
"releases.devUpdateAvailable.action": "Ver release",
|
||||
|
||||
"theme.mode.system": "Sistema",
|
||||
"theme.mode.light": "Claro",
|
||||
"theme.mode.dark": "Oscuro",
|
||||
"theme.toggle.title": "Tema: {mode}",
|
||||
"theme.toggle.ariaLabel": "Tema: {mode}",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ export const commandMessages = {
|
|||
"unifiedPicker.sections.commands": "COMANDOS",
|
||||
"unifiedPicker.sections.agents": "AGENTES",
|
||||
"unifiedPicker.sections.files": "ARCHIVOS",
|
||||
"unifiedPicker.sections.workspaceRoot": "RAÍZ DEL WORKSPACE",
|
||||
"unifiedPicker.badge.subagent": "subagente",
|
||||
"unifiedPicker.footer.navigate": "navegar",
|
||||
"unifiedPicker.footer.select": "seleccionar",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.language.ariaLabel": "Idioma",
|
||||
|
||||
"folderSelection.logoAlt": "Logo de CodeNomad",
|
||||
"folderSelection.tagline": "Selecciona una carpeta para empezar a programar con AI",
|
||||
"folderSelection.tagline": "Selecciona una carpeta para empezar a programar con IA",
|
||||
|
||||
"folderSelection.links.github": "GitHub de CodeNomad",
|
||||
"folderSelection.links.githubStars": "Estrellas de GitHub de CodeNomad",
|
||||
|
|
@ -91,11 +91,11 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.dialog.errorRequired": "El nombre y la URL del servidor son obligatorios.",
|
||||
"folderSelection.servers.dialog.errorConnect": "No se pudo conectar al servidor remoto.",
|
||||
"folderSelection.servers.certificateInstall.title": "Instalar certificado local",
|
||||
"folderSelection.servers.certificateInstall.confirmMessage": "CodeNomad necesita instalar un certificado local para abrir ventanas remotas HTTPS autofirmadas. Este certificado solo se usa para el trafico del proxy local de escritorio en tu equipo. Es posible que tu sistema operativo muestre un segundo aviso de certificado despues de esto.",
|
||||
"folderSelection.servers.certificateInstall.confirmMessage": "CodeNomad necesita instalar un certificado local para abrir ventanas remotas HTTPS autofirmadas. Este certificado solo se usa para el tráfico del proxy local de escritorio en tu equipo. Es posible que tu sistema operativo muestre un segundo aviso de certificado después de esto.",
|
||||
"folderSelection.servers.certificateInstall.confirmLabel": "Continuar",
|
||||
"folderSelection.servers.certificateInstall.cancelLabel": "Cancelar",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "CodeNomad necesita que el certificado local sea de confianza antes de poder abrir ventanas remotas HTTPS autofirmadas.",
|
||||
"folderSelection.sidecars.button": "Open SideCar",
|
||||
"folderSelection.sidecars.button": "Abrir SideCar",
|
||||
|
||||
"projectRenameDialog.title": "Renombrar workspace",
|
||||
"projectRenameDialog.description.withLabel": "Actualiza el nombre del workspace para \"{label}\".",
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ export const instanceMessages = {
|
|||
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
||||
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins que personalizan el comportamiento de la UI y del servidor, y añaden funciones más allá de MCP y LSP.",
|
||||
|
||||
"instanceShell.gitChanges.noSessionSelected": "Selecciona una sesión para ver los cambios de Git.",
|
||||
"instanceShell.gitChanges.loading": "Cargando cambios de Git...",
|
||||
"instanceShell.gitChanges.empty": "Aún no hay cambios de Git.",
|
||||
"instanceShell.gitChanges.deleted": "Eliminado",
|
||||
|
|
@ -141,6 +142,8 @@ export const instanceMessages = {
|
|||
"instanceShell.filesShell.viewerPlaceholder": "La vista detallada se agregará en el siguiente paso.",
|
||||
"instanceShell.filesShell.viewerEmpty": "Ningún archivo seleccionado.",
|
||||
"instanceShell.filesShell.listEmpty": "No hay archivos en esta carpeta.",
|
||||
"instanceShell.filesShell.hideFiles": "Ocultar archivos",
|
||||
"instanceShell.filesShell.showFiles": "Mostrar archivos",
|
||||
"instanceShell.filesShell.search.placeholder": "Filtrar archivos de esta carpeta",
|
||||
"instanceShell.filesShell.search.ariaLabel": "Filtrar archivos de esta carpeta",
|
||||
"instanceShell.filesShell.search.empty": "No hay archivos coincidentes.",
|
||||
|
|
@ -151,6 +154,13 @@ export const instanceMessages = {
|
|||
"instanceShell.filesShell.showSource": "Mostrar fuente",
|
||||
"instanceShell.filesShell.enableWordWrap": "Activar ajuste de línea",
|
||||
"instanceShell.filesShell.disableWordWrap": "Desactivar ajuste de línea",
|
||||
"instanceShell.diff.hideUnchanged": "Ocultar regiones sin cambios",
|
||||
"instanceShell.diff.showFull": "Mostrar archivo completo",
|
||||
"instanceShell.diff.switchToSplit": "Cambiar a vista dividida",
|
||||
"instanceShell.diff.switchToUnified": "Cambiar a vista unificada",
|
||||
"instanceShell.diff.enableWordWrap": "Activar ajuste de línea",
|
||||
"instanceShell.diff.disableWordWrap": "Desactivar ajuste de línea",
|
||||
"instanceShell.worktree.create": "+ Crear worktree",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "Selecciona una sesión para ver el plan.",
|
||||
"instanceShell.plan.empty": "Aún no hay nada planificado.",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const messagingMessages = {
|
|||
"messageSection.loadError.reload": "Recargar mensajes",
|
||||
"messageSection.scroll.toFirstAriaLabel": "Desplazarse al primer mensaje",
|
||||
"messageSection.scroll.toLatestAriaLabel": "Desplazarse al último mensaje",
|
||||
"messageSection.scroll.showControlsAriaLabel": "Mostrar controles de desplazamiento",
|
||||
"messageSection.scroll.enableHoldAriaLabel": "Activar pausa para respuestas largas del asistente",
|
||||
"messageSection.scroll.disableHoldAriaLabel": "Desactivar pausa para respuestas largas del asistente",
|
||||
"messageSection.quote.addAsQuote": "Añadir como cita",
|
||||
|
|
@ -80,6 +81,15 @@ export const messagingMessages = {
|
|||
"messageBlock.reasoning.indicator.hide": "Ocultar",
|
||||
"messageBlock.reasoning.indicator.view": "Ver",
|
||||
"messageBlock.reasoning.detailsAriaLabel": "Detalles del razonamiento",
|
||||
"messageBlock.reasoning.thoughtsFallback": "Pensamientos",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.one": "Pensó durante {count} segundo",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.other": "Pensó durante {count} segundos",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.one": "Pensó durante {count} minuto",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.other": "Pensó durante {count} minutos",
|
||||
"messageBlock.reasoning.thoughtFor.hours.one": "Pensó durante {count} hora",
|
||||
"messageBlock.reasoning.thoughtFor.hours.other": "Pensó durante {count} horas",
|
||||
"messageBlock.reasoning.copyTitle": "Copiar pensamientos",
|
||||
"messageBlock.reasoning.copyAriaLabel": "Copiar pensamientos",
|
||||
|
||||
"codeBlockInline.actions.copy": "Copiar",
|
||||
"codeBlockInline.actions.copied": "¡Copiado!",
|
||||
|
|
@ -88,8 +98,8 @@ export const messagingMessages = {
|
|||
"messageItem.speaker.assistant": "Asistente",
|
||||
"messageItem.actions.revert": "Revertir",
|
||||
"messageItem.actions.revertTitle": "Deshacer cambios hasta aqui (elimina mensajes)",
|
||||
"messageItem.actions.fork": "Fork",
|
||||
"messageItem.actions.forkTitle": "Fork desde este mensaje",
|
||||
"messageItem.actions.fork": "Bifurcar",
|
||||
"messageItem.actions.forkTitle": "Bifurcar desde este mensaje",
|
||||
"messageItem.actions.copy": "Copiar",
|
||||
"messageItem.actions.copyTitle": "Copiar mensaje",
|
||||
"messageItem.actions.copied": "¡Copiado!",
|
||||
|
|
@ -108,6 +118,14 @@ export const messagingMessages = {
|
|||
"messageItem.actions.deleteMessageFailedMessage": "No se pudo eliminar el mensaje",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "Seleccionar mensaje para eliminar",
|
||||
"messageItem.selection.selectForDeletion": "Seleccionar mensaje para eliminar",
|
||||
"messageItem.selection.deselectForDeletion": "Anular selección del mensaje para eliminar",
|
||||
"messageItem.duration.workedFor.seconds.one": "Trabajó durante {count} segundo",
|
||||
"messageItem.duration.workedFor.seconds.other": "Trabajó durante {count} segundos",
|
||||
"messageItem.duration.workedFor.minutes.one": "Trabajó durante {count} minuto",
|
||||
"messageItem.duration.workedFor.minutes.other": "Trabajó durante {count} minutos",
|
||||
"messageItem.duration.workedFor.hours.one": "Trabajó durante {count} hora",
|
||||
"messageItem.duration.workedFor.hours.other": "Trabajó durante {count} horas",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "Elementos seleccionados ({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "Eliminar elementos seleccionados",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const settingsMessages = {
|
|||
"modelSelector.favoritesOnly.showAll": "Mostrar todos los modelos",
|
||||
"modelSelector.favorite.add": "Agregar a favoritos",
|
||||
"modelSelector.favorite.remove": "Quitar de favoritos",
|
||||
"modelSelector.manageProviders": "Manage Providers",
|
||||
"modelSelector.manageProviders": "Gestionar proveedores",
|
||||
|
||||
"thinkingSelector.variant.default": "Por defecto",
|
||||
"thinkingSelector.label": "Pensamiento: {variant}",
|
||||
|
|
@ -64,66 +64,66 @@ export const settingsMessages = {
|
|||
"contextUsagePanel.labels.available": "Disp.",
|
||||
"contextUsagePanel.unavailable": "--",
|
||||
|
||||
"settings.title": "Settings",
|
||||
"settings.navigationAriaLabel": "Settings sections",
|
||||
"settings.close": "Close settings",
|
||||
"settings.content.eyebrow": "Workspace preferences",
|
||||
"settings.open.title": "Open settings",
|
||||
"settings.open.ariaLabel": "Open settings",
|
||||
"settings.nav.appearance": "Appearance",
|
||||
"settings.nav.notifications": "Notifications",
|
||||
"settings.nav.remote": "Remote Access",
|
||||
"settings.nav.speech": "Speech",
|
||||
"settings.nav.providers": "Providers",
|
||||
"settings.title": "Configuración",
|
||||
"settings.navigationAriaLabel": "Secciones de configuración",
|
||||
"settings.close": "Cerrar configuración",
|
||||
"settings.content.eyebrow": "Preferencias del workspace",
|
||||
"settings.open.title": "Abrir configuración",
|
||||
"settings.open.ariaLabel": "Abrir configuración",
|
||||
"settings.nav.appearance": "Apariencia",
|
||||
"settings.nav.notifications": "Notificaciones",
|
||||
"settings.nav.remote": "Acceso remoto",
|
||||
"settings.nav.speech": "Voz",
|
||||
"settings.nav.providers": "Proveedores",
|
||||
"settings.nav.opencode": "OpenCode",
|
||||
"settings.nav.configFiles": "Config Files",
|
||||
"settings.nav.configFiles": "Archivos de configuración",
|
||||
"settings.nav.info": "Info",
|
||||
"settings.scope.device": "This device",
|
||||
"settings.scope.server": "Server setting",
|
||||
"settings.common.enabled": "Enabled",
|
||||
"settings.scope.device": "Este dispositivo",
|
||||
"settings.scope.server": "Configuración del servidor",
|
||||
"settings.common.enabled": "Activado",
|
||||
"settings.common.disabled": "Desactivado",
|
||||
"settings.section.appearance.title": "Appearance",
|
||||
"settings.section.appearance.subtitle": "Adjust how the app looks on this device.",
|
||||
"settings.appearance.theme.title": "Theme",
|
||||
"settings.appearance.theme.subtitle": "Choose the color mode used throughout the app.",
|
||||
"settings.appearance.theme.option.system": "Match your operating system setting",
|
||||
"settings.appearance.theme.option.light": "Use the light appearance",
|
||||
"settings.appearance.theme.option.dark": "Use the dark appearance",
|
||||
"settings.section.notifications.title": "Notifications",
|
||||
"settings.section.notifications.subtitle": "Control OS-level notifications for session activity.",
|
||||
"settings.notifications.permission.granted": "Granted",
|
||||
"settings.notifications.permission.denied": "Denied",
|
||||
"settings.notifications.permission.default": "Not granted",
|
||||
"settings.notifications.permission.unsupported": "Unsupported",
|
||||
"settings.notifications.messages.unsupportedEnvironment": "OS notifications are not supported in this environment.",
|
||||
"settings.notifications.messages.permissionDenied": "Notification permission denied. Enable notifications in your system or browser settings.",
|
||||
"settings.notifications.messages.permissionNotGranted": "Notification permission not granted.",
|
||||
"settings.notifications.messages.unsupportedGeneral": "Notifications are not supported in this environment.",
|
||||
"settings.notifications.messages.permissionGranted": "Permission granted. You can now enable notifications.",
|
||||
"settings.notifications.messages.permissionRequestDenied": "Permission denied. You may need to enable notifications in your system or browser settings.",
|
||||
"settings.notifications.sessionStatus.title": "Session status notifications",
|
||||
"settings.notifications.sessionStatus.subtitle": "Receive alerts when sessions need your attention.",
|
||||
"settings.notifications.enable.title": "Enable notifications",
|
||||
"settings.notifications.enable.permission": "Permission: {permission}",
|
||||
"settings.notifications.requestPermission.title": "Request permission",
|
||||
"settings.notifications.requestPermission.subtitle": "Allow the app to send notifications on this device.",
|
||||
"settings.notifications.requestPermission.action": "Request",
|
||||
"settings.notifications.allowVisible.title": "Notify when the app is focused",
|
||||
"settings.notifications.allowVisible.subtitle": "Keep alerts enabled even while this window is visible.",
|
||||
"settings.notifications.unsupportedNote": "Notifications are not supported in this environment. The notifications control stays disabled.",
|
||||
"settings.notifications.events.title": "Notify me when",
|
||||
"settings.notifications.events.subtitle": "Choose which session events should send alerts.",
|
||||
"settings.notifications.events.needsInput": "Session needs input",
|
||||
"settings.notifications.events.idle": "Session becomes idle",
|
||||
"settings.notifications.status.enabled": "Notifications enabled",
|
||||
"settings.notifications.status.disabled": "Notifications disabled",
|
||||
"settings.notifications.status.unsupported": "Notifications unsupported",
|
||||
"settings.section.remote.title": "Remote Access",
|
||||
"settings.section.remote.subtitle": "Review how this server is exposed on your network and secure access credentials.",
|
||||
"settings.section.appearance.title": "Apariencia",
|
||||
"settings.section.appearance.subtitle": "Ajusta cómo se ve la app en este dispositivo.",
|
||||
"settings.appearance.theme.title": "Tema",
|
||||
"settings.appearance.theme.subtitle": "Elige el modo de color usado en toda la app.",
|
||||
"settings.appearance.theme.option.system": "Coincidir con la configuración del sistema operativo",
|
||||
"settings.appearance.theme.option.light": "Usar apariencia clara",
|
||||
"settings.appearance.theme.option.dark": "Usar apariencia oscura",
|
||||
"settings.section.notifications.title": "Notificaciones",
|
||||
"settings.section.notifications.subtitle": "Controla las notificaciones del sistema para la actividad de sesiones.",
|
||||
"settings.notifications.permission.granted": "Concedido",
|
||||
"settings.notifications.permission.denied": "Denegado",
|
||||
"settings.notifications.permission.default": "No concedido",
|
||||
"settings.notifications.permission.unsupported": "No compatible",
|
||||
"settings.notifications.messages.unsupportedEnvironment": "Las notificaciones del sistema no son compatibles en este entorno.",
|
||||
"settings.notifications.messages.permissionDenied": "Permiso de notificaciones denegado. Activa las notificaciones en la configuración del sistema o del navegador.",
|
||||
"settings.notifications.messages.permissionNotGranted": "Permiso de notificaciones no concedido.",
|
||||
"settings.notifications.messages.unsupportedGeneral": "Las notificaciones no son compatibles en este entorno.",
|
||||
"settings.notifications.messages.permissionGranted": "Permiso concedido. Ahora puedes activar las notificaciones.",
|
||||
"settings.notifications.messages.permissionRequestDenied": "Permiso denegado. Es posible que debas activar las notificaciones en la configuración del sistema o del navegador.",
|
||||
"settings.notifications.sessionStatus.title": "Notificaciones de estado de sesión",
|
||||
"settings.notifications.sessionStatus.subtitle": "Recibe alertas cuando las sesiones necesiten tu atención.",
|
||||
"settings.notifications.enable.title": "Activar notificaciones",
|
||||
"settings.notifications.enable.permission": "Permiso: {permission}",
|
||||
"settings.notifications.requestPermission.title": "Solicitar permiso",
|
||||
"settings.notifications.requestPermission.subtitle": "Permite que la app envíe notificaciones en este dispositivo.",
|
||||
"settings.notifications.requestPermission.action": "Solicitar",
|
||||
"settings.notifications.allowVisible.title": "Notificar cuando la app tenga el foco",
|
||||
"settings.notifications.allowVisible.subtitle": "Mantén las alertas activadas incluso cuando esta ventana esté visible.",
|
||||
"settings.notifications.unsupportedNote": "Las notificaciones no son compatibles en este entorno. El control de notificaciones permanece desactivado.",
|
||||
"settings.notifications.events.title": "Notificarme cuando",
|
||||
"settings.notifications.events.subtitle": "Elige qué eventos de sesión deben enviar alertas.",
|
||||
"settings.notifications.events.needsInput": "La sesión necesita entrada",
|
||||
"settings.notifications.events.idle": "La sesión queda inactiva",
|
||||
"settings.notifications.status.enabled": "Notificaciones activadas",
|
||||
"settings.notifications.status.disabled": "Notificaciones desactivadas",
|
||||
"settings.notifications.status.unsupported": "Notificaciones no compatibles",
|
||||
"settings.section.remote.title": "Acceso remoto",
|
||||
"settings.section.remote.subtitle": "Revisa cómo se expone este servidor en tu red y protege las credenciales de acceso.",
|
||||
"settings.section.opencode.title": "OpenCode",
|
||||
"settings.section.opencode.subtitle": "Choose the OpenCode binary and environment used for new instances.",
|
||||
"settings.section.opencode.subtitle": "Elige el binario de OpenCode y el entorno usados para nuevas instancias.",
|
||||
"settings.opencode.runtime.title": "Runtime",
|
||||
"settings.opencode.runtime.subtitle": "Configure which OpenCode binary new instances launch with.",
|
||||
"settings.opencode.runtime.subtitle": "Configura con qué binario de OpenCode se inician las nuevas instancias.",
|
||||
"settings.opencode.logLevel.title": "Nivel de logs de OpenCode",
|
||||
"settings.opencode.logLevel.subtitle": "Define el nivel de logs usado al iniciar nuevas instancias de OpenCode.",
|
||||
"settings.opencode.logLevel.selector.title": "Verbosidad de logs",
|
||||
|
|
@ -132,131 +132,148 @@ export const settingsMessages = {
|
|||
"settings.opencode.logLevel.option.info": "Informacion",
|
||||
"settings.opencode.logLevel.option.warn": "Advertencia",
|
||||
"settings.opencode.logLevel.option.error": "Error",
|
||||
"settings.configFiles.title": "Config Files",
|
||||
"settings.configFiles.subtitle": "Edit allowlisted global configuration files on this server.",
|
||||
"settings.configFiles.selector.label": "File",
|
||||
"settings.configFiles.selector.placeholder": "Select config file",
|
||||
"settings.configFiles.actions.save": "Save",
|
||||
"settings.configFiles.actions.reload": "Reload",
|
||||
"settings.configFiles.state.unsaved": "Unsaved changes",
|
||||
"settings.configFiles.state.notCreated": "File will be created on save",
|
||||
"settings.configFiles.empty": "No config file selected.",
|
||||
"settings.configFiles.errors.loadList": "Failed to load config files.",
|
||||
"settings.configFiles.errors.loadContent": "Failed to load config file.",
|
||||
"settings.configFiles.errors.save": "Failed to save config file.",
|
||||
"settings.configFiles.toast.saveSuccess": "Config file saved.",
|
||||
"settings.configFiles.toast.saveError": "Failed to save config file.",
|
||||
"settings.configFiles.confirmDiscard.message": "Discard unsaved changes before continuing?",
|
||||
"settings.configFiles.confirmDiscard.confirmLabel": "Discard changes",
|
||||
"settings.configFiles.confirmDiscard.cancelLabel": "Keep editing",
|
||||
"settings.configFiles.title": "Archivos de configuración",
|
||||
"settings.configFiles.subtitle": "Edita archivos de configuración global permitidos en este servidor.",
|
||||
"settings.configFiles.selector.label": "Archivo",
|
||||
"settings.configFiles.selector.placeholder": "Seleccionar archivo de configuración",
|
||||
"settings.configFiles.actions.save": "Guardar",
|
||||
"settings.configFiles.actions.reload": "Recargar",
|
||||
"settings.configFiles.state.unsaved": "Cambios sin guardar",
|
||||
"settings.configFiles.state.notCreated": "El archivo se creará al guardar",
|
||||
"settings.configFiles.empty": "No hay ningún archivo de configuración seleccionado.",
|
||||
"settings.configFiles.errors.loadList": "No se pudieron cargar los archivos de configuración.",
|
||||
"settings.configFiles.errors.loadContent": "No se pudo cargar el archivo de configuración.",
|
||||
"settings.configFiles.errors.save": "No se pudo guardar el archivo de configuración.",
|
||||
"settings.configFiles.toast.saveSuccess": "Archivo de configuración guardado.",
|
||||
"settings.configFiles.toast.saveError": "No se pudo guardar el archivo de configuración.",
|
||||
"settings.configFiles.confirmDiscard.message": "¿Descartar los cambios sin guardar antes de continuar?",
|
||||
"settings.configFiles.confirmDiscard.confirmLabel": "Descartar cambios",
|
||||
"settings.configFiles.confirmDiscard.cancelLabel": "Seguir editando",
|
||||
|
||||
"settings.providers.title": "Provider Authentication",
|
||||
"settings.providers.subtitle": "Connect API keys or OAuth accounts for the active OpenCode instance.",
|
||||
"settings.providers.refresh": "Refresh",
|
||||
"settings.providers.selectProvider": "Select provider",
|
||||
"settings.providers.available.title": "Available Providers",
|
||||
"settings.providers.configured.title": "Configured Providers",
|
||||
"settings.providers.loading": "Loading provider authentication methods...",
|
||||
"settings.providers.empty.noInstance": "Start an OpenCode workspace before managing provider authentication.",
|
||||
"settings.providers.empty.noProviders": "No providers are available from this OpenCode instance.",
|
||||
"settings.providers.empty.noConfiguredProviders": "No providers are currently configured for this OpenCode instance.",
|
||||
"settings.providers.models.one": "{count} model",
|
||||
"settings.providers.models.other": "{count} models",
|
||||
"settings.providers.actions.connect": "Connect",
|
||||
"settings.providers.actions.disconnect": "Disconnect",
|
||||
"settings.providers.actions.remove": "Remove saved auth",
|
||||
"settings.providers.actions.close": "Close",
|
||||
"settings.providers.actions.continue": "Continue",
|
||||
"settings.providers.actions.working": "Working...",
|
||||
"settings.providers.method.title": "Authentication method",
|
||||
"settings.providers.method.subtitle": "Choose the flow OpenCode exposes for this provider.",
|
||||
"settings.providers.method.api": "API key",
|
||||
"settings.providers.title": "Autenticación de proveedores",
|
||||
"settings.providers.subtitle": "Conecta claves API o cuentas OAuth para la instancia activa de OpenCode.",
|
||||
"settings.providers.refresh": "Actualizar",
|
||||
"settings.providers.selectProvider": "Seleccionar proveedor",
|
||||
"settings.providers.available.title": "Proveedores disponibles",
|
||||
"settings.providers.configured.title": "Proveedores configurados",
|
||||
"settings.providers.loading": "Cargando métodos de autenticación de proveedores...",
|
||||
"settings.providers.empty.noInstance": "Inicia un workspace de OpenCode antes de gestionar la autenticación de proveedores.",
|
||||
"settings.providers.empty.noProviders": "No hay proveedores disponibles desde esta instancia de OpenCode.",
|
||||
"settings.providers.empty.noConfiguredProviders": "No hay proveedores configurados actualmente para esta instancia de OpenCode.",
|
||||
"settings.providers.models.one": "{count} modelo",
|
||||
"settings.providers.models.other": "{count} modelos",
|
||||
"settings.providers.actions.connect": "Conectar",
|
||||
"settings.providers.actions.disconnect": "Desconectar",
|
||||
"settings.providers.actions.remove": "Quitar autenticación guardada",
|
||||
"settings.providers.actions.close": "Cerrar",
|
||||
"settings.providers.actions.continue": "Continuar",
|
||||
"settings.providers.actions.working": "Trabajando...",
|
||||
"settings.providers.method.title": "Método de autenticación",
|
||||
"settings.providers.method.subtitle": "Elige el flujo que OpenCode expone para este proveedor.",
|
||||
"settings.providers.method.api": "Clave API",
|
||||
"settings.providers.method.oauth": "OAuth",
|
||||
"settings.providers.method.mixed": "API key and OAuth",
|
||||
"settings.providers.method.fallback": "API key",
|
||||
"settings.providers.status.available": "Available",
|
||||
"settings.providers.auth.title": "Connect {provider}",
|
||||
"settings.providers.auth.subtitle": "Authentication is saved inside the active OpenCode server.",
|
||||
"settings.providers.apiKey.label": "API key",
|
||||
"settings.providers.apiKey.placeholder": "Paste provider API key",
|
||||
"settings.providers.oauth.noPrompts": "This OAuth method does not need additional details before opening the browser.",
|
||||
"settings.providers.oauth.promptsUnsupported": "This OAuth method requires provider-specific prompt inputs that are not supported by the current SDK build.",
|
||||
"settings.providers.oauth.enterCode": "Complete the browser flow, then paste the authorization code.",
|
||||
"settings.providers.oauth.openPage": "Open authorization page",
|
||||
"settings.providers.oauth.openHere": "Open in this tab",
|
||||
"settings.providers.oauth.copyLink": "Copy link",
|
||||
"settings.providers.oauth.linkCopied": "Link copied",
|
||||
"settings.providers.oauth.popupBlocked": "Your browser may have blocked the authorization page. Use the actions above to continue the sign-in flow.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "OpenCode authorization",
|
||||
"settings.providers.oauth.popup.loadingBody": "Preparing authorization page...",
|
||||
"settings.providers.oauth.codeLabel": "Authorization code",
|
||||
"settings.providers.oauth.codePlaceholder": "Paste code from provider",
|
||||
"settings.providers.oauth.submitCode": "Submit code",
|
||||
"settings.providers.oauth.waitingTitle": "Waiting for authorization",
|
||||
"settings.providers.oauth.cancelWait": "Cancel",
|
||||
"settings.providers.prompt.selectPlaceholder": "Select an option",
|
||||
"settings.providers.success": "Provider authentication saved. Provider data has been refreshed.",
|
||||
"settings.providers.source.api": "Saved provider authentication",
|
||||
"settings.providers.source.config": "Configured in opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configured by custom provider logic",
|
||||
"settings.providers.source.env": "Configured from environment variables",
|
||||
"settings.providers.source.unknown": "Configured provider source",
|
||||
"settings.providers.errors.loadFailed": "Failed to load provider authentication methods.",
|
||||
"settings.providers.errors.noAuthorization": "Provider did not return authorization data.",
|
||||
"settings.providers.errors.authorizationFailed": "Authorization failed.",
|
||||
"settings.providers.errors.removeFailed": "Failed to remove provider authentication.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "This provider is configured from environment variables and cannot be disconnected from the UI.",
|
||||
"settings.providers.method.mixed": "Clave API y OAuth",
|
||||
"settings.providers.method.fallback": "Clave API",
|
||||
"settings.providers.status.available": "Disponible",
|
||||
"settings.providers.auth.title": "Conectar {provider}",
|
||||
"settings.providers.auth.subtitle": "La autenticación se guarda dentro del servidor activo de OpenCode.",
|
||||
"settings.providers.apiKey.label": "Clave API",
|
||||
"settings.providers.apiKey.placeholder": "Pega la clave API del proveedor",
|
||||
"settings.providers.oauth.noPrompts": "Este método OAuth no necesita detalles adicionales antes de abrir el navegador.",
|
||||
"settings.providers.oauth.promptsUnsupported": "Este método OAuth requiere entradas específicas del proveedor que la build actual del SDK no admite.",
|
||||
"settings.providers.oauth.enterCode": "Completa el flujo del navegador y luego pega el código de autorización.",
|
||||
"settings.providers.oauth.openPage": "Abrir página de autorización",
|
||||
"settings.providers.oauth.openHere": "Abrir en esta pestaña",
|
||||
"settings.providers.oauth.copyLink": "Copiar enlace",
|
||||
"settings.providers.oauth.linkCopied": "Enlace copiado",
|
||||
"settings.providers.oauth.popupBlocked": "Es posible que tu navegador haya bloqueado la página de autorización. Usa las acciones de arriba para continuar el inicio de sesión.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "Autorización de OpenCode",
|
||||
"settings.providers.oauth.popup.loadingBody": "Preparando página de autorización...",
|
||||
"settings.providers.oauth.codeLabel": "Código de autorización",
|
||||
"settings.providers.oauth.codePlaceholder": "Pega el código del proveedor",
|
||||
"settings.providers.oauth.submitCode": "Enviar código",
|
||||
"settings.providers.oauth.waitingTitle": "Esperando autorización",
|
||||
"settings.providers.oauth.cancelWait": "Cancelar",
|
||||
"settings.providers.prompt.selectPlaceholder": "Selecciona una opción",
|
||||
"settings.providers.success": "Autenticación del proveedor guardada. Los datos del proveedor se actualizaron.",
|
||||
"settings.providers.source.api": "Autenticación de proveedor guardada",
|
||||
"settings.providers.source.config": "Configurado en opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configurado por lógica personalizada del proveedor",
|
||||
"settings.providers.source.env": "Configurado desde variables de entorno",
|
||||
"settings.providers.source.unknown": "Origen de proveedor configurado",
|
||||
"settings.providers.errors.loadFailed": "No se pudieron cargar los métodos de autenticación de proveedores.",
|
||||
"settings.providers.errors.noAuthorization": "El proveedor no devolvió datos de autorización.",
|
||||
"settings.providers.errors.authorizationFailed": "La autorización falló.",
|
||||
"settings.providers.errors.removeFailed": "No se pudo quitar la autenticación del proveedor.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "Este proveedor está configurado desde variables de entorno y no se puede desconectar desde la UI.",
|
||||
|
||||
"settings.appearance.behavior.title": "Interaccion",
|
||||
"settings.appearance.behavior.title": "Interacción",
|
||||
"settings.appearance.behavior.subtitle": "Valores predeterminados de mensajes, diffs y entrada.",
|
||||
"settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets",
|
||||
"settings.behavior.expansionPreset.minimal.title": "Minimal",
|
||||
"settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.",
|
||||
"settings.behavior.expansionPreset.balanced.title": "Balanced",
|
||||
"settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.",
|
||||
"settings.behavior.expansionPreset.detailed.title": "Detailed",
|
||||
"settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.",
|
||||
"settings.behavior.expansionPreset.everything.title": "Everything",
|
||||
"settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.",
|
||||
"settings.behavior.expansionPreset.custom.title": "Custom",
|
||||
"settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.",
|
||||
"settings.behavior.expansionDefaults.title": "Expansion defaults",
|
||||
"settings.behavior.expansionDefaults.itemColumn": "Item",
|
||||
"settings.behavior.expansionDefaults.stateColumn": "Default state",
|
||||
"settings.behavior.expansionDefaults.thinking": "thinking",
|
||||
"settings.behavior.expansionDefaults.otherTools": "Other tools",
|
||||
"settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}",
|
||||
"settings.behavior.keyboardHints.title": "Sugerencias de atajos de teclado",
|
||||
"settings.behavior.keyboardHints.subtitle": "Muestra sugerencias de atajos de teclado en toda la interfaz.",
|
||||
"settings.behavior.thinking.title": "Secciones de pensamiento",
|
||||
"settings.behavior.thinking.subtitle": "Muestra u oculta las secciones de pensamiento de la IA en los mensajes.",
|
||||
"settings.behavior.thinkingDefault.title": "Pensamiento por defecto",
|
||||
"settings.behavior.thinkingDefault.subtitle": "Elige si las secciones de pensamiento comienzan expandidas o contraidas.",
|
||||
"settings.behavior.messageTimeline.title": "Linea de tiempo de mensajes",
|
||||
"settings.behavior.messageTimeline.subtitle": "Muestra u oculta la barra lateral de la linea de tiempo de mensajes.",
|
||||
"settings.behavior.timelineTools.title": "Llamadas de herramientas en la linea de tiempo",
|
||||
"settings.behavior.timelineTools.subtitle": "Muestra u oculta entradas de llamadas de herramientas en la linea de tiempo de mensajes.",
|
||||
"settings.behavior.thinkingDefault.subtitle": "Elige si las secciones de pensamiento comienzan expandidas o contraídas.",
|
||||
"settings.behavior.messageTimeline.title": "Línea de tiempo de mensajes",
|
||||
"settings.behavior.messageTimeline.subtitle": "Muestra u oculta la barra lateral de la línea de tiempo de mensajes.",
|
||||
"settings.behavior.timelineTools.title": "Llamadas de herramientas en la línea de tiempo",
|
||||
"settings.behavior.timelineTools.subtitle": "Muestra u oculta entradas de llamadas de herramientas en la línea de tiempo de mensajes.",
|
||||
"settings.behavior.diffView.title": "Vista de diferencias",
|
||||
"settings.behavior.diffView.subtitle": "Elige como se muestran los diffs de llamadas de herramientas.",
|
||||
"settings.behavior.diffView.subtitle": "Elige cómo se muestran los diffs de llamadas de herramientas.",
|
||||
"settings.behavior.diffView.option.split": "Dividida",
|
||||
"settings.behavior.diffView.option.unified": "Unificada",
|
||||
"settings.behavior.toolOutputsDefault.title": "Salidas de herramientas por defecto",
|
||||
"settings.behavior.toolOutputsDefault.subtitle": "Elige si las salidas de herramientas comienzan expandidas o contraidas.",
|
||||
"settings.behavior.diagnosticsDefault.title": "Diagnosticos por defecto",
|
||||
"settings.behavior.diagnosticsDefault.subtitle": "Elige si la salida de diagnosticos comienza expandida o contraida.",
|
||||
"settings.behavior.toolOutputsDefault.subtitle": "Elige si las salidas de herramientas comienzan expandidas o contraídas.",
|
||||
"settings.behavior.diagnosticsDefault.title": "Diagnósticos por defecto",
|
||||
"settings.behavior.diagnosticsDefault.subtitle": "Elige si la salida de diagnósticos comienza expandida o contraída.",
|
||||
"settings.behavior.toolInputsVisibility.title": "Visibilidad de entradas de herramientas",
|
||||
"settings.behavior.toolInputsVisibility.subtitle": "Establece la visibilidad por defecto de los argumentos de entrada de las llamadas de herramientas.",
|
||||
"settings.behavior.usageMetrics.title": "Metricas de uso de tokens",
|
||||
"settings.behavior.usageMetrics.subtitle": "Muestra u oculta estadisticas de tokens y costo en mensajes del asistente.",
|
||||
"settings.behavior.autoCleanup.title": "Limpieza automatica de sesiones en blanco",
|
||||
"settings.behavior.autoCleanup.subtitle": "Limpia automaticamente las sesiones en blanco al crear nuevas.",
|
||||
"settings.behavior.usageMetrics.title": "Métricas de uso de tokens",
|
||||
"settings.behavior.usageMetrics.subtitle": "Muestra u oculta estadísticas de tokens y costo en mensajes del asistente.",
|
||||
"settings.behavior.autoCleanup.title": "Limpieza automática de sesiones en blanco",
|
||||
"settings.behavior.autoCleanup.subtitle": "Limpia automáticamente las sesiones en blanco al crear nuevas.",
|
||||
"settings.behavior.keepUnseenSubagentIdle.title": "Mantener marcadores idle de subagentes",
|
||||
"settings.behavior.keepUnseenSubagentIdle.subtitle": "Mantiene visibles los marcadores idle de subagentes hasta verlos, en lugar de ocultarlos despues de 5 segundos.",
|
||||
"settings.behavior.keepUnseenSubagentIdle.subtitle": "Mantiene visibles los marcadores idle de subagentes hasta verlos, en lugar de ocultarlos después de 5 segundos.",
|
||||
"settings.behavior.tauriNativeEventTransport.title": "Transporte de eventos nativo de Tauri",
|
||||
"settings.behavior.tauriNativeEventTransport.subtitle": "Usa el transporte de eventos de escritorio nativo en Rust dentro de Tauri. Desactivalo para volver a la ruta browser EventSource.",
|
||||
"settings.behavior.promptVoiceInput.title": "Prompt voice input",
|
||||
"settings.behavior.promptVoiceInput.subtitle": "Show the microphone control for speech-to-text prompt input when speech is configured.",
|
||||
"settings.behavior.tauriNativeEventTransport.subtitle": "Usa el transporte de eventos de escritorio nativo en Rust dentro de Tauri. Desactívalo para volver a la ruta browser EventSource.",
|
||||
"settings.behavior.promptVoiceInput.title": "Entrada de voz del prompt",
|
||||
"settings.behavior.promptVoiceInput.subtitle": "Muestra el control del micrófono para la entrada de voz a texto del prompt cuando la voz esté configurada.",
|
||||
"settings.behavior.promptSubmit.title": "Enter para enviar",
|
||||
"settings.behavior.promptSubmit.subtitle": "Usa Enter para enviar; Cmd/Ctrl+Enter inserta una nueva linea.",
|
||||
"settings.behavior.promptSubmit.subtitle": "Usa Enter para enviar; Cmd/Ctrl+Enter inserta una nueva línea.",
|
||||
"settings.speech.title": "Voz",
|
||||
"settings.speech.subtitle": "Configura ahora el reconocimiento de voz y prepara la base de texto a voz para funciones futuras.",
|
||||
"settings.speech.provider.title": "Proveedor",
|
||||
"settings.speech.provider.subtitle": "Las solicitudes de voz usan el adaptador de voz del servidor.",
|
||||
"settings.speech.provider.openaiCompatible": "OpenAI-compatible",
|
||||
"settings.speech.provider.openaiCompatible": "Compatible con OpenAI",
|
||||
"settings.speech.status.loading": "Comprobando configuración...",
|
||||
"settings.speech.status.configured": "Configurado",
|
||||
"settings.speech.status.missing": "Falta la clave API",
|
||||
"settings.speech.status.error": "Servicio de voz no disponible",
|
||||
"settings.speech.apiKey.title": "API key",
|
||||
"settings.speech.apiKey.title": "Clave API",
|
||||
"settings.speech.apiKey.subtitle": "Se usa para las solicitudes de voz gestionadas por CodeNomad.",
|
||||
"settings.speech.apiKey.placeholder": "Introduce una nueva clave API",
|
||||
"settings.speech.apiKey.storedNote": "Hay una clave API guardada y oculta. Introduce un nuevo valor para reemplazarla o deja el campo vacío para conservarla.",
|
||||
"settings.speech.apiKey.clearAction": "Borrar clave guardada",
|
||||
"settings.speech.apiKey.clearPending": "La clave API guardada se eliminará al guardar.",
|
||||
"settings.speech.baseUrl.title": "Base URL",
|
||||
"settings.speech.baseUrl.title": "URL base",
|
||||
"settings.speech.baseUrl.subtitle": "Anulación opcional para endpoints de voz compatibles con OpenAI.",
|
||||
"settings.speech.baseUrl.placeholder": "https://api.openai.com/v1",
|
||||
"settings.speech.sttModel.title": "Modelo de transcripción",
|
||||
|
|
@ -265,62 +282,62 @@ export const settingsMessages = {
|
|||
"settings.speech.ttsModel.subtitle": "Modelo predeterminado de texto a voz reservado para futuras funciones de reproducción.",
|
||||
"settings.speech.ttsVoice.title": "Voz predeterminada",
|
||||
"settings.speech.ttsVoice.subtitle": "Voz predeterminada de texto a voz reservada para futuras funciones de reproducción.",
|
||||
"settings.speech.playbackMode.title": "Modo de reproduccion",
|
||||
"settings.speech.playbackMode.subtitle": "Elige si TTS empieza a reproducirse mientras llega el audio o despues de generar el archivo completo.",
|
||||
"settings.speech.playbackMode.streaming": "Streaming",
|
||||
"settings.speech.playbackMode.buffered": "Buffered",
|
||||
"settings.speech.playbackMode.title": "Modo de reproducción",
|
||||
"settings.speech.playbackMode.subtitle": "Elige si TTS empieza a reproducirse mientras llega el audio o después de generar el archivo completo.",
|
||||
"settings.speech.playbackMode.streaming": "En streaming",
|
||||
"settings.speech.playbackMode.buffered": "Con búfer",
|
||||
"settings.speech.ttsFormat.title": "Formato de salida",
|
||||
"settings.speech.ttsFormat.subtitle": "Elige el formato de audio para la voz sintetizada. La compatibilidad de streaming depende de tu proveedor y navegador.",
|
||||
"settings.speech.help": "La entrada de voz del prompt aparece cuando la transcripcion de voz esta configurada y es compatible. La reproduccion de mensajes usa el modo y formato TTS seleccionados aqui.",
|
||||
"settings.speech.compatibility.streamingUnavailable": "Tu configuracion actual del proveedor de voz no anuncia TTS por streaming. Cambia el modo de reproduccion a buffered si quieres que la reproduccion funcione ahora.",
|
||||
"settings.speech.compatibility.browserStreamingUnavailable": "Tu navegador actual no puede reproducir por streaming el formato TTS seleccionado. Elige reproduccion buffered o cambia a otro formato.",
|
||||
"settings.speech.compatibility.runtimeNote": "Todos los formatos siguen disponibles en modo streaming. Algunas combinaciones de navegador y proveedor aun pueden fallar al reproducir.",
|
||||
"settings.speech.testPlayback.action": "Probar reproduccion",
|
||||
"settings.speech.help": "La entrada de voz del prompt aparece cuando la transcripción de voz está configurada y es compatible. La reproducción de mensajes usa el modo y formato TTS seleccionados aquí.",
|
||||
"settings.speech.compatibility.streamingUnavailable": "Tu configuración actual del proveedor de voz no anuncia TTS por streaming. Cambia el modo de reproducción a con búfer si quieres que la reproducción funcione ahora.",
|
||||
"settings.speech.compatibility.browserStreamingUnavailable": "Tu navegador actual no puede reproducir por streaming el formato TTS seleccionado. Elige reproducción con búfer o cambia a otro formato.",
|
||||
"settings.speech.compatibility.runtimeNote": "Todos los formatos siguen disponibles en modo streaming. Algunas combinaciones de navegador y proveedor aún pueden fallar al reproducir.",
|
||||
"settings.speech.testPlayback.action": "Probar reproducción",
|
||||
"settings.speech.testPlayback.generating": "Generando muestra",
|
||||
"settings.speech.testPlayback.stop": "Detener muestra",
|
||||
"settings.speech.testPlayback.sample": "Gracias por usar CodeNomad, tu configuracion de voz funciona correctamente.",
|
||||
"settings.speech.testPlayback.note": "La prueba usa de inmediato el modo y formato actuales. Guarda primero los cambios de API key, base URL, modelo o voz si tambien quieres probarlos.",
|
||||
"settings.speech.testPlayback.sample": "Gracias por usar CodeNomad, tu configuración de voz funciona correctamente.",
|
||||
"settings.speech.testPlayback.note": "La prueba usa de inmediato el modo y formato actuales. Guarda primero los cambios de clave API, URL base, modelo o voz si también quieres probarlos.",
|
||||
"settings.speech.save.action": "Guardar",
|
||||
"settings.speech.save.saving": "Guardando...",
|
||||
"settings.speech.save.saved": "Guardado",
|
||||
"settings.speech.save.unsaved": "Cambios sin guardar",
|
||||
"settings.speech.save.error": "Error al guardar",
|
||||
"settings.nav.sidecars": "SideCars",
|
||||
"settings.section.sidecars.eyebrow": "Server services",
|
||||
"settings.section.sidecars.eyebrow": "Servicios del servidor",
|
||||
"settings.section.sidecars.title": "SideCars",
|
||||
"settings.section.sidecars.subtitle": "Configure local services listening on fixed ports that the server can proxy into tabs.",
|
||||
"sidecars.form.name": "Name",
|
||||
"sidecars.form.validation": "Enter a valid SideCar name and port.",
|
||||
"sidecars.form.port": "Port",
|
||||
"sidecars.form.insecure": "Use HTTP",
|
||||
"sidecars.form.protocol": "Protocol",
|
||||
"sidecars.form.protocol.help": "Choose how the proxy should connect to the local service.",
|
||||
"settings.section.sidecars.subtitle": "Configura servicios locales que escuchan en puertos fijos y que el servidor puede proxificar en pestañas.",
|
||||
"sidecars.form.name": "Nombre",
|
||||
"sidecars.form.validation": "Introduce un nombre y puerto de SideCar válidos.",
|
||||
"sidecars.form.port": "Puerto",
|
||||
"sidecars.form.insecure": "Usar HTTP",
|
||||
"sidecars.form.protocol": "Protocolo",
|
||||
"sidecars.form.protocol.help": "Elige cómo debe conectarse el proxy al servicio local.",
|
||||
"sidecars.form.protocol.https": "HTTPS",
|
||||
"sidecars.form.protocol.http": "HTTP",
|
||||
"sidecars.form.prefixMode": "Prefix mode",
|
||||
"sidecars.form.prefixMode.help": "Choose whether the SideCar receives the public /sidecars path prefix.",
|
||||
"sidecars.form.prefixMode.strip": "Strip prefix",
|
||||
"sidecars.form.prefixMode.preserve": "Preserve prefix",
|
||||
"sidecars.form.add": "Add SideCar",
|
||||
"sidecars.kind.port": "Port",
|
||||
"sidecars.status.running": "Running",
|
||||
"sidecars.status.stopped": "Stopped",
|
||||
"sidecars.basePath": "Base path",
|
||||
"sidecars.settings.listTitle": "Configured SideCars",
|
||||
"sidecars.settings.listSubtitle": "Review the port-based SideCars available in the picker.",
|
||||
"sidecars.settings.empty": "No SideCars configured yet.",
|
||||
"sidecars.picker.title": "Open SideCar",
|
||||
"sidecars.picker.loading": "Loading SideCars...",
|
||||
"sidecars.picker.subtitle": "Choose an available SideCar to open in a new tab.",
|
||||
"sidecars.picker.empty": "No port-based SideCars are available yet.",
|
||||
"sidecars.picker.close": "Close",
|
||||
"sidecars.open.errorTitle": "Unable to open SideCar",
|
||||
"sidecars.open.notFound": "SideCar not found.",
|
||||
"sidecars.open.notRunning": "SideCar is not reachable on its configured port.",
|
||||
"sidecars.back": "Back",
|
||||
"sidecars.refresh": "Refresh",
|
||||
"sidecars.path": "Path",
|
||||
"sidecars.go": "Go",
|
||||
"sidecars.form.prefixMode": "Modo de prefijo",
|
||||
"sidecars.form.prefixMode.help": "Elige si el SideCar recibe el prefijo de ruta pública /sidecars.",
|
||||
"sidecars.form.prefixMode.strip": "Quitar prefijo",
|
||||
"sidecars.form.prefixMode.preserve": "Conservar prefijo",
|
||||
"sidecars.form.add": "Añadir SideCar",
|
||||
"sidecars.kind.port": "Puerto",
|
||||
"sidecars.status.running": "En ejecución",
|
||||
"sidecars.status.stopped": "Detenido",
|
||||
"sidecars.basePath": "Ruta base",
|
||||
"sidecars.settings.listTitle": "SideCars configurados",
|
||||
"sidecars.settings.listSubtitle": "Revisa los SideCars basados en puertos disponibles en el selector.",
|
||||
"sidecars.settings.empty": "Aún no hay SideCars configurados.",
|
||||
"sidecars.picker.title": "Abrir SideCar",
|
||||
"sidecars.picker.loading": "Cargando SideCars...",
|
||||
"sidecars.picker.subtitle": "Elige un SideCar disponible para abrirlo en una pestaña nueva.",
|
||||
"sidecars.picker.empty": "Aún no hay SideCars basados en puertos disponibles.",
|
||||
"sidecars.picker.close": "Cerrar",
|
||||
"sidecars.open.errorTitle": "No se pudo abrir el SideCar",
|
||||
"sidecars.open.notFound": "No se encontró el SideCar.",
|
||||
"sidecars.open.notRunning": "No se puede acceder al SideCar en su puerto configurado.",
|
||||
"sidecars.back": "Volver",
|
||||
"sidecars.refresh": "Actualizar",
|
||||
"sidecars.path": "Ruta",
|
||||
"sidecars.go": "Ir",
|
||||
|
||||
// Toast History Panel
|
||||
"toastHistory.title": "Historial de notificaciones",
|
||||
|
|
@ -343,25 +360,25 @@ export const settingsMessages = {
|
|||
"toastHistory.deleteItem": "Eliminar notificación",
|
||||
|
||||
// Info Section
|
||||
"settings.section.info.title": "About",
|
||||
"settings.section.info.subtitle": "View version, runtime, and gather diagnostic information.",
|
||||
"settings.info.version.server": "Server version",
|
||||
"settings.info.version.ui": "UI version",
|
||||
"settings.info.version.uiSource": "UI source",
|
||||
"settings.section.info.title": "Acerca de",
|
||||
"settings.section.info.subtitle": "Consulta la versión, el runtime y recopila información de diagnóstico.",
|
||||
"settings.info.version.server": "Versión del servidor",
|
||||
"settings.info.version.ui": "Versión de la UI",
|
||||
"settings.info.version.uiSource": "Origen de la UI",
|
||||
"settings.info.runtime.type": "Runtime",
|
||||
"settings.info.runtime.platform": "Platform",
|
||||
"settings.info.runtime.os": "Operating system",
|
||||
"settings.info.server.url": "Server URL",
|
||||
"settings.info.server.root": "Workspace root",
|
||||
"settings.info.updates.title": "Updates",
|
||||
"settings.info.updates.subtitle": "Check for new releases of CodeNomad.",
|
||||
"settings.info.updates.latest": "Latest version",
|
||||
"settings.info.updates.download": "Download update",
|
||||
"settings.info.updates.refresh": "Refresh",
|
||||
"settings.info.diagnostics.copyFailed": "Clipboard access denied. Use the download button instead.",
|
||||
"settings.info.diagnostics.title": "Diagnostics",
|
||||
"settings.info.runtime.platform": "Plataforma",
|
||||
"settings.info.runtime.os": "Sistema operativo",
|
||||
"settings.info.server.url": "URL del servidor",
|
||||
"settings.info.server.root": "Raíz del workspace",
|
||||
"settings.info.updates.title": "Actualizaciones",
|
||||
"settings.info.updates.subtitle": "Busca nuevas releases de CodeNomad.",
|
||||
"settings.info.updates.latest": "Última versión",
|
||||
"settings.info.updates.download": "Descargar actualización",
|
||||
"settings.info.updates.refresh": "Actualizar",
|
||||
"settings.info.diagnostics.copyFailed": "Acceso al portapapeles denegado. Usa el botón de descarga en su lugar.",
|
||||
"settings.info.diagnostics.title": "Diagnósticos",
|
||||
"settings.info.diagnostics.subtitle": "Recopila información del sistema y de la versión para informes de errores.",
|
||||
"settings.info.diagnostics.copy": "Copy to clipboard",
|
||||
"settings.info.diagnostics.download": "Download .txt",
|
||||
"settings.info.diagnostics.copied": "Diagnostic info copied to clipboard.",
|
||||
"settings.info.diagnostics.copy": "Copiar al portapapeles",
|
||||
"settings.info.diagnostics.download": "Descargar .txt",
|
||||
"settings.info.diagnostics.copied": "Información de diagnóstico copiada al portapapeles.",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -2,16 +2,22 @@ export const toolCallMessages = {
|
|||
"toolCall.pending.waitingToRun": "Esperando para ejecutar...",
|
||||
"toolCall.error.label": "Error:",
|
||||
|
||||
"toolCall.header.copyTitle": "Copy tool call title",
|
||||
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
||||
"toolCall.header.copyTitle": "Copiar título de la llamada de herramienta",
|
||||
"toolCall.header.copyAriaLabel": "Copiar título de la llamada de herramienta",
|
||||
"toolCall.header.copyOutputTitle": "Copiar salida de herramienta",
|
||||
"toolCall.header.copyOutputAriaLabel": "Copiar salida de herramienta",
|
||||
|
||||
"toolCall.header.showInputTitle": "Show Tool Arguments",
|
||||
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
|
||||
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
|
||||
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
|
||||
"toolCall.header.showInputTitle": "Mostrar argumentos de herramienta",
|
||||
"toolCall.header.showInputAriaLabel": "Mostrar argumentos de herramienta",
|
||||
"toolCall.header.hideInputTitle": "Ocultar argumentos de herramienta",
|
||||
"toolCall.header.hideInputAriaLabel": "Ocultar argumentos de herramienta",
|
||||
|
||||
"toolCall.io.input": "Tool Input",
|
||||
"toolCall.io.output": "Tool Output",
|
||||
"toolCall.io.input": "Entrada de herramienta",
|
||||
"toolCall.io.output": "Salida de herramienta",
|
||||
"toolCall.io.copyInputTitle": "Copiar entrada de herramienta",
|
||||
"toolCall.io.copyInputAriaLabel": "Copiar entrada de herramienta",
|
||||
"toolCall.io.copyOutputTitle": "Copiar salida de herramienta",
|
||||
"toolCall.io.copyOutputAriaLabel": "Copiar salida de herramienta",
|
||||
|
||||
"toolCall.diff.label": "Diff",
|
||||
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||
|
|
@ -53,7 +59,7 @@ export const toolCallMessages = {
|
|||
"toolCall.renderer.read.detail.offset": "Desplazamiento: {offset}",
|
||||
"toolCall.renderer.read.detail.limit": "Límite: {limit}",
|
||||
|
||||
"toolCall.renderer.todo.empty": "Aún no hay elementos del plan.",
|
||||
"toolCall.renderer.todo.empty": "No hay elementos planificados",
|
||||
"toolCall.renderer.todo.status.pending": "Pendiente",
|
||||
"toolCall.renderer.todo.status.inProgress": "En progreso",
|
||||
"toolCall.renderer.todo.status.completed": "Completado",
|
||||
|
|
@ -71,13 +77,9 @@ export const toolCallMessages = {
|
|||
"toolCall.permission.actions.allowOnce": "Permitir una vez",
|
||||
"toolCall.permission.actions.alwaysAllow": "Permitir siempre",
|
||||
"toolCall.permission.actions.deny": "Denegar",
|
||||
"toolCall.permission.actions.confirmDeny": "Confirmar denegación",
|
||||
"toolCall.permission.actions.cancel": "Cancelar",
|
||||
"toolCall.permission.shortcuts.allowOnce": "Permitir una vez",
|
||||
"toolCall.permission.shortcuts.alwaysAllow": "Permitir siempre",
|
||||
"toolCall.permission.rejectReason.label": "¿Por qué denegar este permiso?",
|
||||
"toolCall.permission.rejectReason.placeholder": "Indica a CodeNomad qué debe hacer de otra forma...",
|
||||
"toolCall.permission.rejectReason.hint": "El agente verá estos comentarios y podrá ajustar su siguiente acción.",
|
||||
"toolCall.permission.rejectReason.placeholder": "Comentario opcional al denegar, p. ej. usa un comando más seguro...",
|
||||
"toolCall.permission.errors.unableToUpdate": "No se pudo actualizar el permiso",
|
||||
|
||||
"permissionApproval.title": "Solicitudes",
|
||||
|
|
@ -94,11 +96,7 @@ export const toolCallMessages = {
|
|||
"permissionApproval.actions.allowOnce": "Permitir una vez",
|
||||
"permissionApproval.actions.alwaysAllow": "Permitir siempre",
|
||||
"permissionApproval.actions.deny": "Denegar",
|
||||
"permissionApproval.actions.confirmDeny": "Confirmar denegación",
|
||||
"permissionApproval.actions.cancel": "Cancelar",
|
||||
"permissionApproval.rejectReason.label": "¿Por qué denegar este permiso?",
|
||||
"permissionApproval.rejectReason.placeholder": "Indica a CodeNomad qué debe hacer de otra forma...",
|
||||
"permissionApproval.rejectReason.hint": "El agente verá estos comentarios y podrá ajustar su siguiente acción.",
|
||||
"permissionApproval.rejectReason.placeholder": "Comentario opcional al denegar, p. ej. usa un comando más seguro...",
|
||||
"permissionApproval.fallbackHint": "Carga la sesión para más información.",
|
||||
"permissionApproval.errors.unableToUpdatePermission": "No se pudo actualizar el permiso",
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const appMessages = {
|
|||
"app.stopInstance.title": "Arrêter l'instance",
|
||||
"app.stopInstance.confirmLabel": "Arrêter",
|
||||
"app.stopInstance.cancelLabel": "Laisser tourner",
|
||||
"app.stopInstance.toast.error": "Impossible d'arrêter l'espace de travail.",
|
||||
|
||||
"emptyState.logoAlt": "Logo CodeNomad",
|
||||
"emptyState.brandTitle": "CodeNomad",
|
||||
|
|
@ -33,4 +34,10 @@ export const appMessages = {
|
|||
"releases.devUpdateAvailable.title": "Build dev disponible",
|
||||
"releases.devUpdateAvailable.message": "Un nouveau build dev est disponible : {version}.",
|
||||
"releases.devUpdateAvailable.action": "Voir la release",
|
||||
|
||||
"theme.mode.system": "Système",
|
||||
"theme.mode.light": "Clair",
|
||||
"theme.mode.dark": "Sombre",
|
||||
"theme.toggle.title": "Thème : {mode}",
|
||||
"theme.toggle.ariaLabel": "Thème : {mode}",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ export const commandMessages = {
|
|||
"unifiedPicker.sections.commands": "COMMANDES",
|
||||
"unifiedPicker.sections.agents": "AGENTS",
|
||||
"unifiedPicker.sections.files": "FICHIERS",
|
||||
"unifiedPicker.sections.workspaceRoot": "RACINE DE L'ESPACE DE TRAVAIL",
|
||||
"unifiedPicker.badge.subagent": "sous-agent",
|
||||
"unifiedPicker.footer.navigate": "naviguer",
|
||||
"unifiedPicker.footer.select": "sélectionner",
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.certificateInstall.confirmLabel": "Continuer",
|
||||
"folderSelection.servers.certificateInstall.cancelLabel": "Annuler",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "CodeNomad a besoin que le certificat local soit approuve avant de pouvoir ouvrir des fenetres distantes HTTPS auto-signees.",
|
||||
"folderSelection.sidecars.button": "Open SideCar",
|
||||
"folderSelection.sidecars.button": "Ouvrir SideCar",
|
||||
|
||||
"projectRenameDialog.title": "Renommer l'espace de travail",
|
||||
"projectRenameDialog.description.withLabel": "Mettez à jour le nom de l'espace de travail pour \"{label}\".",
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ export const instanceMessages = {
|
|||
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
||||
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins qui personnalisent le comportement de l'UI et du serveur, ajoutant des fonctionnalités au-delà de MCP et LSP.",
|
||||
|
||||
"instanceShell.gitChanges.noSessionSelected": "Sélectionnez une session pour voir les changements Git.",
|
||||
"instanceShell.gitChanges.loading": "Chargement des changements Git...",
|
||||
"instanceShell.gitChanges.empty": "Aucun changement Git pour l'instant.",
|
||||
"instanceShell.gitChanges.deleted": "Supprimé",
|
||||
|
|
@ -151,6 +152,15 @@ export const instanceMessages = {
|
|||
"instanceShell.filesShell.showSource": "Afficher la source",
|
||||
"instanceShell.filesShell.enableWordWrap": "Activer le retour à la ligne",
|
||||
"instanceShell.filesShell.disableWordWrap": "Désactiver le retour à la ligne",
|
||||
"instanceShell.filesShell.hideFiles": "Masquer les fichiers",
|
||||
"instanceShell.filesShell.showFiles": "Afficher les fichiers",
|
||||
"instanceShell.diff.hideUnchanged": "Masquer les zones inchangées",
|
||||
"instanceShell.diff.showFull": "Afficher le fichier complet",
|
||||
"instanceShell.diff.switchToSplit": "Passer à la vue côte à côte",
|
||||
"instanceShell.diff.switchToUnified": "Passer à la vue unifiée",
|
||||
"instanceShell.diff.enableWordWrap": "Activer le retour à la ligne",
|
||||
"instanceShell.diff.disableWordWrap": "Désactiver le retour à la ligne",
|
||||
"instanceShell.worktree.create": "+ Créer un worktree",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "Sélectionnez une session pour voir le plan.",
|
||||
"instanceShell.plan.empty": "Aucun plan pour l'instant.",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const messagingMessages = {
|
|||
"messageSection.loadError.reload": "Recharger les messages",
|
||||
"messageSection.scroll.toFirstAriaLabel": "Aller au premier message",
|
||||
"messageSection.scroll.toLatestAriaLabel": "Aller au dernier message",
|
||||
"messageSection.scroll.showControlsAriaLabel": "Afficher les contrôles de défilement",
|
||||
"messageSection.scroll.enableHoldAriaLabel": "Activer le maintien pour les longues réponses de l'assistant",
|
||||
"messageSection.scroll.disableHoldAriaLabel": "Désactiver le maintien pour les longues réponses de l'assistant",
|
||||
"messageSection.quote.addAsQuote": "Ajouter en citation",
|
||||
|
|
@ -43,7 +44,7 @@ export const messagingMessages = {
|
|||
"messageTimeline.ariaLabel": "Chronologie des messages",
|
||||
"messageTimeline.segment.user.label": "Vous",
|
||||
"messageTimeline.segment.assistant.label": "Asst",
|
||||
"messageTimeline.segment.compaction.label": "Compaction",
|
||||
"messageTimeline.segment.compaction.label": "Compactage",
|
||||
"messageTimeline.tool.fallbackLabel": "Appel d'outil",
|
||||
"messageTimeline.tooltip.userFallback": "Message utilisateur",
|
||||
"messageTimeline.tooltip.assistantFallback": "Réponse de l'assistant",
|
||||
|
|
@ -80,6 +81,15 @@ export const messagingMessages = {
|
|||
"messageBlock.reasoning.indicator.hide": "Masquer",
|
||||
"messageBlock.reasoning.indicator.view": "Voir",
|
||||
"messageBlock.reasoning.detailsAriaLabel": "Détails du raisonnement",
|
||||
"messageBlock.reasoning.thoughtsFallback": "Réflexions",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.one": "Réflexion pendant {count} seconde",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.other": "Réflexion pendant {count} secondes",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.one": "Réflexion pendant {count} minute",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.other": "Réflexion pendant {count} minutes",
|
||||
"messageBlock.reasoning.thoughtFor.hours.one": "Réflexion pendant {count} heure",
|
||||
"messageBlock.reasoning.thoughtFor.hours.other": "Réflexion pendant {count} heures",
|
||||
"messageBlock.reasoning.copyTitle": "Copier les réflexions",
|
||||
"messageBlock.reasoning.copyAriaLabel": "Copier les réflexions",
|
||||
|
||||
"codeBlockInline.actions.copy": "Copier",
|
||||
"codeBlockInline.actions.copied": "Copié !",
|
||||
|
|
@ -88,8 +98,8 @@ export const messagingMessages = {
|
|||
"messageItem.speaker.assistant": "Assistant",
|
||||
"messageItem.actions.revert": "Revenir",
|
||||
"messageItem.actions.revertTitle": "Annuler les changements jusqu'ici (supprime les messages)",
|
||||
"messageItem.actions.fork": "Fork",
|
||||
"messageItem.actions.forkTitle": "Fork depuis ce message",
|
||||
"messageItem.actions.fork": "Bifurquer",
|
||||
"messageItem.actions.forkTitle": "Bifurquer depuis ce message",
|
||||
"messageItem.actions.copy": "Copier",
|
||||
"messageItem.actions.copyTitle": "Copier le message",
|
||||
"messageItem.actions.copied": "Copié !",
|
||||
|
|
@ -108,6 +118,14 @@ export const messagingMessages = {
|
|||
"messageItem.actions.deleteMessageFailedMessage": "Impossible de supprimer le message",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "Sélectionner le message pour suppression",
|
||||
"messageItem.selection.selectForDeletion": "Sélectionner le message pour suppression",
|
||||
"messageItem.selection.deselectForDeletion": "Désélectionner le message pour suppression",
|
||||
"messageItem.duration.workedFor.seconds.one": "A travaillé pendant {count} seconde",
|
||||
"messageItem.duration.workedFor.seconds.other": "A travaillé pendant {count} secondes",
|
||||
"messageItem.duration.workedFor.minutes.one": "A travaillé pendant {count} minute",
|
||||
"messageItem.duration.workedFor.minutes.other": "A travaillé pendant {count} minutes",
|
||||
"messageItem.duration.workedFor.hours.one": "A travaillé pendant {count} heure",
|
||||
"messageItem.duration.workedFor.hours.other": "A travaillé pendant {count} heures",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "Éléments sélectionnés ({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "Supprimer les éléments sélectionnés",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const settingsMessages = {
|
|||
"modelSelector.favoritesOnly.showAll": "Afficher tous les modèles",
|
||||
"modelSelector.favorite.add": "Ajouter aux favoris",
|
||||
"modelSelector.favorite.remove": "Retirer des favoris",
|
||||
"modelSelector.manageProviders": "Manage Providers",
|
||||
"modelSelector.manageProviders": "Gérer les fournisseurs",
|
||||
|
||||
"thinkingSelector.variant.default": "Par défaut",
|
||||
"thinkingSelector.label": "Réflexion : {variant}",
|
||||
|
|
@ -64,66 +64,66 @@ export const settingsMessages = {
|
|||
"contextUsagePanel.labels.available": "Dispo",
|
||||
"contextUsagePanel.unavailable": "--",
|
||||
|
||||
"settings.title": "Settings",
|
||||
"settings.navigationAriaLabel": "Settings sections",
|
||||
"settings.close": "Close settings",
|
||||
"settings.content.eyebrow": "Workspace preferences",
|
||||
"settings.open.title": "Open settings",
|
||||
"settings.open.ariaLabel": "Open settings",
|
||||
"settings.nav.appearance": "Appearance",
|
||||
"settings.title": "Paramètres",
|
||||
"settings.navigationAriaLabel": "Sections des paramètres",
|
||||
"settings.close": "Fermer les paramètres",
|
||||
"settings.content.eyebrow": "Préférences de l'espace de travail",
|
||||
"settings.open.title": "Ouvrir les paramètres",
|
||||
"settings.open.ariaLabel": "Ouvrir les paramètres",
|
||||
"settings.nav.appearance": "Apparence",
|
||||
"settings.nav.notifications": "Notifications",
|
||||
"settings.nav.remote": "Remote Access",
|
||||
"settings.nav.speech": "Speech",
|
||||
"settings.nav.providers": "Providers",
|
||||
"settings.nav.remote": "Accès distant",
|
||||
"settings.nav.speech": "Voix",
|
||||
"settings.nav.providers": "Fournisseurs",
|
||||
"settings.nav.opencode": "OpenCode",
|
||||
"settings.nav.configFiles": "Config Files",
|
||||
"settings.nav.configFiles": "Fichiers de configuration",
|
||||
"settings.nav.info": "Info",
|
||||
"settings.scope.device": "This device",
|
||||
"settings.scope.server": "Server setting",
|
||||
"settings.common.enabled": "Enabled",
|
||||
"settings.common.disabled": "Desactive",
|
||||
"settings.section.appearance.title": "Appearance",
|
||||
"settings.section.appearance.subtitle": "Adjust how the app looks on this device.",
|
||||
"settings.appearance.theme.title": "Theme",
|
||||
"settings.appearance.theme.subtitle": "Choose the color mode used throughout the app.",
|
||||
"settings.appearance.theme.option.system": "Match your operating system setting",
|
||||
"settings.appearance.theme.option.light": "Use the light appearance",
|
||||
"settings.appearance.theme.option.dark": "Use the dark appearance",
|
||||
"settings.scope.device": "Cet appareil",
|
||||
"settings.scope.server": "Paramètre serveur",
|
||||
"settings.common.enabled": "Activé",
|
||||
"settings.common.disabled": "Désactivé",
|
||||
"settings.section.appearance.title": "Apparence",
|
||||
"settings.section.appearance.subtitle": "Ajustez l'apparence de l'application sur cet appareil.",
|
||||
"settings.appearance.theme.title": "Thème",
|
||||
"settings.appearance.theme.subtitle": "Choisissez le mode de couleur utilisé dans toute l'application.",
|
||||
"settings.appearance.theme.option.system": "Suivre le réglage de votre système d'exploitation",
|
||||
"settings.appearance.theme.option.light": "Utiliser l'apparence claire",
|
||||
"settings.appearance.theme.option.dark": "Utiliser l'apparence sombre",
|
||||
"settings.section.notifications.title": "Notifications",
|
||||
"settings.section.notifications.subtitle": "Control OS-level notifications for session activity.",
|
||||
"settings.notifications.permission.granted": "Granted",
|
||||
"settings.notifications.permission.denied": "Denied",
|
||||
"settings.notifications.permission.default": "Not granted",
|
||||
"settings.notifications.permission.unsupported": "Unsupported",
|
||||
"settings.notifications.messages.unsupportedEnvironment": "OS notifications are not supported in this environment.",
|
||||
"settings.notifications.messages.permissionDenied": "Notification permission denied. Enable notifications in your system or browser settings.",
|
||||
"settings.notifications.messages.permissionNotGranted": "Notification permission not granted.",
|
||||
"settings.notifications.messages.unsupportedGeneral": "Notifications are not supported in this environment.",
|
||||
"settings.notifications.messages.permissionGranted": "Permission granted. You can now enable notifications.",
|
||||
"settings.notifications.messages.permissionRequestDenied": "Permission denied. You may need to enable notifications in your system or browser settings.",
|
||||
"settings.notifications.sessionStatus.title": "Session status notifications",
|
||||
"settings.notifications.sessionStatus.subtitle": "Receive alerts when sessions need your attention.",
|
||||
"settings.notifications.enable.title": "Enable notifications",
|
||||
"settings.notifications.enable.permission": "Permission: {permission}",
|
||||
"settings.notifications.requestPermission.title": "Request permission",
|
||||
"settings.notifications.requestPermission.subtitle": "Allow the app to send notifications on this device.",
|
||||
"settings.notifications.requestPermission.action": "Request",
|
||||
"settings.notifications.allowVisible.title": "Notify when the app is focused",
|
||||
"settings.notifications.allowVisible.subtitle": "Keep alerts enabled even while this window is visible.",
|
||||
"settings.notifications.unsupportedNote": "Notifications are not supported in this environment. The notifications control stays disabled.",
|
||||
"settings.notifications.events.title": "Notify me when",
|
||||
"settings.notifications.events.subtitle": "Choose which session events should send alerts.",
|
||||
"settings.notifications.events.needsInput": "Session needs input",
|
||||
"settings.notifications.events.idle": "Session becomes idle",
|
||||
"settings.notifications.status.enabled": "Notifications enabled",
|
||||
"settings.notifications.status.disabled": "Notifications disabled",
|
||||
"settings.notifications.status.unsupported": "Notifications unsupported",
|
||||
"settings.section.remote.title": "Remote Access",
|
||||
"settings.section.remote.subtitle": "Review how this server is exposed on your network and secure access credentials.",
|
||||
"settings.section.notifications.subtitle": "Contrôlez les notifications système liées à l'activité des sessions.",
|
||||
"settings.notifications.permission.granted": "Accordée",
|
||||
"settings.notifications.permission.denied": "Refusée",
|
||||
"settings.notifications.permission.default": "Non accordée",
|
||||
"settings.notifications.permission.unsupported": "Non prise en charge",
|
||||
"settings.notifications.messages.unsupportedEnvironment": "Les notifications système ne sont pas prises en charge dans cet environnement.",
|
||||
"settings.notifications.messages.permissionDenied": "Autorisation de notification refusée. Activez les notifications dans les paramètres de votre système ou navigateur.",
|
||||
"settings.notifications.messages.permissionNotGranted": "Autorisation de notification non accordée.",
|
||||
"settings.notifications.messages.unsupportedGeneral": "Les notifications ne sont pas prises en charge dans cet environnement.",
|
||||
"settings.notifications.messages.permissionGranted": "Autorisation accordée. Vous pouvez maintenant activer les notifications.",
|
||||
"settings.notifications.messages.permissionRequestDenied": "Autorisation refusée. Vous devrez peut-être activer les notifications dans les paramètres de votre système ou navigateur.",
|
||||
"settings.notifications.sessionStatus.title": "Notifications d'état de session",
|
||||
"settings.notifications.sessionStatus.subtitle": "Recevez des alertes lorsque des sessions nécessitent votre attention.",
|
||||
"settings.notifications.enable.title": "Activer les notifications",
|
||||
"settings.notifications.enable.permission": "Autorisation : {permission}",
|
||||
"settings.notifications.requestPermission.title": "Demander l'autorisation",
|
||||
"settings.notifications.requestPermission.subtitle": "Autoriser l'application à envoyer des notifications sur cet appareil.",
|
||||
"settings.notifications.requestPermission.action": "Demander",
|
||||
"settings.notifications.allowVisible.title": "Notifier lorsque l'application est au premier plan",
|
||||
"settings.notifications.allowVisible.subtitle": "Garder les alertes activées même lorsque cette fenêtre est visible.",
|
||||
"settings.notifications.unsupportedNote": "Les notifications ne sont pas prises en charge dans cet environnement. Le contrôle des notifications reste désactivé.",
|
||||
"settings.notifications.events.title": "Me notifier lorsque",
|
||||
"settings.notifications.events.subtitle": "Choisissez les événements de session qui doivent envoyer des alertes.",
|
||||
"settings.notifications.events.needsInput": "Une session nécessite une saisie",
|
||||
"settings.notifications.events.idle": "Une session devient inactive",
|
||||
"settings.notifications.status.enabled": "Notifications activées",
|
||||
"settings.notifications.status.disabled": "Notifications désactivées",
|
||||
"settings.notifications.status.unsupported": "Notifications non prises en charge",
|
||||
"settings.section.remote.title": "Accès distant",
|
||||
"settings.section.remote.subtitle": "Vérifiez comment ce serveur est exposé sur votre réseau et sécurisez les identifiants d'accès.",
|
||||
"settings.section.opencode.title": "OpenCode",
|
||||
"settings.section.opencode.subtitle": "Choose the OpenCode binary and environment used for new instances.",
|
||||
"settings.opencode.runtime.title": "Runtime",
|
||||
"settings.opencode.runtime.subtitle": "Configure which OpenCode binary new instances launch with.",
|
||||
"settings.section.opencode.subtitle": "Choisissez le binaire OpenCode et l'environnement utilisés pour les nouvelles instances.",
|
||||
"settings.opencode.runtime.title": "Environnement d'exécution",
|
||||
"settings.opencode.runtime.subtitle": "Configurez le binaire OpenCode avec lequel les nouvelles instances démarrent.",
|
||||
"settings.opencode.logLevel.title": "Niveau de logs OpenCode",
|
||||
"settings.opencode.logLevel.subtitle": "Definir le niveau de logs utilise au lancement des nouvelles instances OpenCode.",
|
||||
"settings.opencode.logLevel.selector.title": "Verbosite des logs",
|
||||
|
|
@ -132,83 +132,100 @@ export const settingsMessages = {
|
|||
"settings.opencode.logLevel.option.info": "Info",
|
||||
"settings.opencode.logLevel.option.warn": "Avertissement",
|
||||
"settings.opencode.logLevel.option.error": "Erreur",
|
||||
"settings.configFiles.title": "Config Files",
|
||||
"settings.configFiles.subtitle": "Edit allowlisted global configuration files on this server.",
|
||||
"settings.configFiles.selector.label": "File",
|
||||
"settings.configFiles.selector.placeholder": "Select config file",
|
||||
"settings.configFiles.actions.save": "Save",
|
||||
"settings.configFiles.actions.reload": "Reload",
|
||||
"settings.configFiles.state.unsaved": "Unsaved changes",
|
||||
"settings.configFiles.state.notCreated": "File will be created on save",
|
||||
"settings.configFiles.empty": "No config file selected.",
|
||||
"settings.configFiles.errors.loadList": "Failed to load config files.",
|
||||
"settings.configFiles.errors.loadContent": "Failed to load config file.",
|
||||
"settings.configFiles.errors.save": "Failed to save config file.",
|
||||
"settings.configFiles.toast.saveSuccess": "Config file saved.",
|
||||
"settings.configFiles.toast.saveError": "Failed to save config file.",
|
||||
"settings.configFiles.confirmDiscard.message": "Discard unsaved changes before continuing?",
|
||||
"settings.configFiles.confirmDiscard.confirmLabel": "Discard changes",
|
||||
"settings.configFiles.confirmDiscard.cancelLabel": "Keep editing",
|
||||
"settings.configFiles.title": "Fichiers de configuration",
|
||||
"settings.configFiles.subtitle": "Modifiez les fichiers de configuration globaux autorisés sur ce serveur.",
|
||||
"settings.configFiles.selector.label": "Fichier",
|
||||
"settings.configFiles.selector.placeholder": "Sélectionner un fichier de configuration",
|
||||
"settings.configFiles.actions.save": "Enregistrer",
|
||||
"settings.configFiles.actions.reload": "Recharger",
|
||||
"settings.configFiles.state.unsaved": "Modifications non enregistrées",
|
||||
"settings.configFiles.state.notCreated": "Le fichier sera créé lors de l'enregistrement",
|
||||
"settings.configFiles.empty": "Aucun fichier de configuration sélectionné.",
|
||||
"settings.configFiles.errors.loadList": "Échec du chargement des fichiers de configuration.",
|
||||
"settings.configFiles.errors.loadContent": "Échec du chargement du fichier de configuration.",
|
||||
"settings.configFiles.errors.save": "Échec de l'enregistrement du fichier de configuration.",
|
||||
"settings.configFiles.toast.saveSuccess": "Fichier de configuration enregistré.",
|
||||
"settings.configFiles.toast.saveError": "Échec de l'enregistrement du fichier de configuration.",
|
||||
"settings.configFiles.confirmDiscard.message": "Ignorer les modifications non enregistrées avant de continuer ?",
|
||||
"settings.configFiles.confirmDiscard.confirmLabel": "Ignorer les modifications",
|
||||
"settings.configFiles.confirmDiscard.cancelLabel": "Continuer la modification",
|
||||
|
||||
"settings.providers.title": "Provider Authentication",
|
||||
"settings.providers.subtitle": "Connect API keys or OAuth accounts for the active OpenCode instance.",
|
||||
"settings.providers.refresh": "Refresh",
|
||||
"settings.providers.selectProvider": "Select provider",
|
||||
"settings.providers.available.title": "Available Providers",
|
||||
"settings.providers.configured.title": "Configured Providers",
|
||||
"settings.providers.loading": "Loading provider authentication methods...",
|
||||
"settings.providers.empty.noInstance": "Start an OpenCode workspace before managing provider authentication.",
|
||||
"settings.providers.empty.noProviders": "No providers are available from this OpenCode instance.",
|
||||
"settings.providers.empty.noConfiguredProviders": "No providers are currently configured for this OpenCode instance.",
|
||||
"settings.providers.models.one": "{count} model",
|
||||
"settings.providers.models.other": "{count} models",
|
||||
"settings.providers.actions.connect": "Connect",
|
||||
"settings.providers.actions.disconnect": "Disconnect",
|
||||
"settings.providers.actions.remove": "Remove saved auth",
|
||||
"settings.providers.actions.close": "Close",
|
||||
"settings.providers.actions.continue": "Continue",
|
||||
"settings.providers.actions.working": "Working...",
|
||||
"settings.providers.method.title": "Authentication method",
|
||||
"settings.providers.method.subtitle": "Choose the flow OpenCode exposes for this provider.",
|
||||
"settings.providers.method.api": "API key",
|
||||
"settings.providers.title": "Authentification des fournisseurs",
|
||||
"settings.providers.subtitle": "Connectez des clés API ou des comptes OAuth pour l'instance OpenCode active.",
|
||||
"settings.providers.refresh": "Actualiser",
|
||||
"settings.providers.selectProvider": "Sélectionner un fournisseur",
|
||||
"settings.providers.available.title": "Fournisseurs disponibles",
|
||||
"settings.providers.configured.title": "Fournisseurs configurés",
|
||||
"settings.providers.loading": "Chargement des méthodes d'authentification des fournisseurs...",
|
||||
"settings.providers.empty.noInstance": "Démarrez un espace de travail OpenCode avant de gérer l'authentification des fournisseurs.",
|
||||
"settings.providers.empty.noProviders": "Aucun fournisseur n'est disponible depuis cette instance OpenCode.",
|
||||
"settings.providers.empty.noConfiguredProviders": "Aucun fournisseur n'est actuellement configuré pour cette instance OpenCode.",
|
||||
"settings.providers.models.one": "{count} modèle",
|
||||
"settings.providers.models.other": "{count} modèles",
|
||||
"settings.providers.actions.connect": "Connecter",
|
||||
"settings.providers.actions.disconnect": "Déconnecter",
|
||||
"settings.providers.actions.remove": "Supprimer l'authentification enregistrée",
|
||||
"settings.providers.actions.close": "Fermer",
|
||||
"settings.providers.actions.continue": "Continuer",
|
||||
"settings.providers.actions.working": "Traitement...",
|
||||
"settings.providers.method.title": "Méthode d'authentification",
|
||||
"settings.providers.method.subtitle": "Choisissez le flux exposé par OpenCode pour ce fournisseur.",
|
||||
"settings.providers.method.api": "Clé API",
|
||||
"settings.providers.method.oauth": "OAuth",
|
||||
"settings.providers.method.mixed": "API key and OAuth",
|
||||
"settings.providers.method.fallback": "API key",
|
||||
"settings.providers.status.available": "Available",
|
||||
"settings.providers.auth.title": "Connect {provider}",
|
||||
"settings.providers.auth.subtitle": "Authentication is saved inside the active OpenCode server.",
|
||||
"settings.providers.apiKey.label": "API key",
|
||||
"settings.providers.apiKey.placeholder": "Paste provider API key",
|
||||
"settings.providers.oauth.noPrompts": "This OAuth method does not need additional details before opening the browser.",
|
||||
"settings.providers.oauth.promptsUnsupported": "This OAuth method requires provider-specific prompt inputs that are not supported by the current SDK build.",
|
||||
"settings.providers.oauth.enterCode": "Complete the browser flow, then paste the authorization code.",
|
||||
"settings.providers.oauth.openPage": "Open authorization page",
|
||||
"settings.providers.oauth.openHere": "Open in this tab",
|
||||
"settings.providers.oauth.copyLink": "Copy link",
|
||||
"settings.providers.oauth.linkCopied": "Link copied",
|
||||
"settings.providers.oauth.popupBlocked": "Your browser may have blocked the authorization page. Use the actions above to continue the sign-in flow.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "OpenCode authorization",
|
||||
"settings.providers.oauth.popup.loadingBody": "Preparing authorization page...",
|
||||
"settings.providers.oauth.codeLabel": "Authorization code",
|
||||
"settings.providers.oauth.codePlaceholder": "Paste code from provider",
|
||||
"settings.providers.oauth.submitCode": "Submit code",
|
||||
"settings.providers.oauth.waitingTitle": "Waiting for authorization",
|
||||
"settings.providers.oauth.cancelWait": "Cancel",
|
||||
"settings.providers.prompt.selectPlaceholder": "Select an option",
|
||||
"settings.providers.success": "Provider authentication saved. Provider data has been refreshed.",
|
||||
"settings.providers.source.api": "Saved provider authentication",
|
||||
"settings.providers.source.config": "Configured in opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configured by custom provider logic",
|
||||
"settings.providers.source.env": "Configured from environment variables",
|
||||
"settings.providers.source.unknown": "Configured provider source",
|
||||
"settings.providers.errors.loadFailed": "Failed to load provider authentication methods.",
|
||||
"settings.providers.errors.noAuthorization": "Provider did not return authorization data.",
|
||||
"settings.providers.errors.authorizationFailed": "Authorization failed.",
|
||||
"settings.providers.errors.removeFailed": "Failed to remove provider authentication.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "This provider is configured from environment variables and cannot be disconnected from the UI.",
|
||||
"settings.providers.method.mixed": "Clé API et OAuth",
|
||||
"settings.providers.method.fallback": "Clé API",
|
||||
"settings.providers.status.available": "Disponible",
|
||||
"settings.providers.auth.title": "Connecter {provider}",
|
||||
"settings.providers.auth.subtitle": "L'authentification est enregistrée dans le serveur OpenCode actif.",
|
||||
"settings.providers.apiKey.label": "Clé API",
|
||||
"settings.providers.apiKey.placeholder": "Coller la clé API du fournisseur",
|
||||
"settings.providers.oauth.noPrompts": "Cette méthode OAuth ne nécessite aucun détail supplémentaire avant l'ouverture du navigateur.",
|
||||
"settings.providers.oauth.promptsUnsupported": "Cette méthode OAuth nécessite des saisies propres au fournisseur qui ne sont pas prises en charge par la version actuelle du SDK.",
|
||||
"settings.providers.oauth.enterCode": "Terminez le flux dans le navigateur, puis collez le code d'autorisation.",
|
||||
"settings.providers.oauth.openPage": "Ouvrir la page d'autorisation",
|
||||
"settings.providers.oauth.openHere": "Ouvrir dans cet onglet",
|
||||
"settings.providers.oauth.copyLink": "Copier le lien",
|
||||
"settings.providers.oauth.linkCopied": "Lien copié",
|
||||
"settings.providers.oauth.popupBlocked": "Votre navigateur a peut-être bloqué la page d'autorisation. Utilisez les actions ci-dessus pour continuer le flux de connexion.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "Autorisation OpenCode",
|
||||
"settings.providers.oauth.popup.loadingBody": "Préparation de la page d'autorisation...",
|
||||
"settings.providers.oauth.codeLabel": "Code d'autorisation",
|
||||
"settings.providers.oauth.codePlaceholder": "Coller le code du fournisseur",
|
||||
"settings.providers.oauth.submitCode": "Envoyer le code",
|
||||
"settings.providers.oauth.waitingTitle": "En attente de l'autorisation",
|
||||
"settings.providers.oauth.cancelWait": "Annuler",
|
||||
"settings.providers.prompt.selectPlaceholder": "Sélectionner une option",
|
||||
"settings.providers.success": "Authentification du fournisseur enregistrée. Les données du fournisseur ont été actualisées.",
|
||||
"settings.providers.source.api": "Authentification du fournisseur enregistrée",
|
||||
"settings.providers.source.config": "Configuré dans opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configuré par une logique de fournisseur personnalisée",
|
||||
"settings.providers.source.env": "Configuré depuis les variables d'environnement",
|
||||
"settings.providers.source.unknown": "Source du fournisseur configuré",
|
||||
"settings.providers.errors.loadFailed": "Échec du chargement des méthodes d'authentification des fournisseurs.",
|
||||
"settings.providers.errors.noAuthorization": "Le fournisseur n'a pas renvoyé de données d'autorisation.",
|
||||
"settings.providers.errors.authorizationFailed": "Échec de l'autorisation.",
|
||||
"settings.providers.errors.removeFailed": "Échec de la suppression de l'authentification du fournisseur.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "Ce fournisseur est configuré depuis des variables d'environnement et ne peut pas être déconnecté depuis l'interface.",
|
||||
|
||||
"settings.appearance.behavior.title": "Interaction",
|
||||
"settings.appearance.behavior.subtitle": "Parametres par defaut pour les messages, les diffs et la saisie.",
|
||||
"settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets",
|
||||
"settings.behavior.expansionPreset.minimal.title": "Minimal",
|
||||
"settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.",
|
||||
"settings.behavior.expansionPreset.balanced.title": "Balanced",
|
||||
"settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.",
|
||||
"settings.behavior.expansionPreset.detailed.title": "Detailed",
|
||||
"settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.",
|
||||
"settings.behavior.expansionPreset.everything.title": "Everything",
|
||||
"settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.",
|
||||
"settings.behavior.expansionPreset.custom.title": "Custom",
|
||||
"settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.",
|
||||
"settings.behavior.expansionDefaults.title": "Expansion defaults",
|
||||
"settings.behavior.expansionDefaults.itemColumn": "Item",
|
||||
"settings.behavior.expansionDefaults.stateColumn": "Default state",
|
||||
"settings.behavior.expansionDefaults.thinking": "thinking",
|
||||
"settings.behavior.expansionDefaults.otherTools": "Other tools",
|
||||
"settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}",
|
||||
"settings.behavior.keyboardHints.title": "Indications de raccourcis clavier",
|
||||
"settings.behavior.keyboardHints.subtitle": "Afficher des indications de raccourcis clavier dans toute l'interface.",
|
||||
"settings.behavior.thinking.title": "Sections de reflexion",
|
||||
|
|
@ -237,26 +254,26 @@ export const settingsMessages = {
|
|||
"settings.behavior.keepUnseenSubagentIdle.subtitle": "Garde les marqueurs inactifs des sous-agents visibles jusqu'a consultation au lieu de les masquer apres 5 secondes.",
|
||||
"settings.behavior.tauriNativeEventTransport.title": "Transport d'evenements natif Tauri",
|
||||
"settings.behavior.tauriNativeEventTransport.subtitle": "Utiliser le transport d'evenements desktop natif Rust dans Tauri. Desactivez-le pour revenir au chemin browser EventSource.",
|
||||
"settings.behavior.promptVoiceInput.title": "Prompt voice input",
|
||||
"settings.behavior.promptVoiceInput.subtitle": "Show the microphone control for speech-to-text prompt input when speech is configured.",
|
||||
"settings.behavior.promptVoiceInput.title": "Saisie vocale du prompt",
|
||||
"settings.behavior.promptVoiceInput.subtitle": "Afficher le contrôle du microphone pour la saisie vocale du prompt lorsque la voix est configurée.",
|
||||
"settings.behavior.promptSubmit.title": "Entrer pour envoyer",
|
||||
"settings.behavior.promptSubmit.subtitle": "Utiliser Entrer pour envoyer; Cmd/Ctrl+Entrer insere une nouvelle ligne.",
|
||||
"settings.speech.title": "Voix",
|
||||
"settings.speech.subtitle": "Configurez dès maintenant la reconnaissance vocale et préparez la synthèse vocale pour de futures fonctionnalités.",
|
||||
"settings.speech.provider.title": "Fournisseur",
|
||||
"settings.speech.provider.subtitle": "Les requêtes vocales utilisent l'adaptateur vocal côté serveur.",
|
||||
"settings.speech.provider.openaiCompatible": "OpenAI-compatible",
|
||||
"settings.speech.provider.openaiCompatible": "Compatible OpenAI",
|
||||
"settings.speech.status.loading": "Vérification de la configuration...",
|
||||
"settings.speech.status.configured": "Configuré",
|
||||
"settings.speech.status.missing": "Clé API manquante",
|
||||
"settings.speech.status.error": "Service vocal indisponible",
|
||||
"settings.speech.apiKey.title": "API key",
|
||||
"settings.speech.apiKey.title": "Clé API",
|
||||
"settings.speech.apiKey.subtitle": "Utilisée pour les requêtes vocales gérées par CodeNomad.",
|
||||
"settings.speech.apiKey.placeholder": "Saisissez une nouvelle clé API",
|
||||
"settings.speech.apiKey.storedNote": "Une clé API enregistrée est masquée. Saisissez une nouvelle valeur pour la remplacer ou laissez le champ vide pour la conserver.",
|
||||
"settings.speech.apiKey.clearAction": "Effacer la clé enregistrée",
|
||||
"settings.speech.apiKey.clearPending": "La clé API enregistrée sera supprimée lors de l'enregistrement.",
|
||||
"settings.speech.baseUrl.title": "Base URL",
|
||||
"settings.speech.baseUrl.title": "URL de base",
|
||||
"settings.speech.baseUrl.subtitle": "Remplacement facultatif des points d'accès vocaux compatibles OpenAI.",
|
||||
"settings.speech.baseUrl.placeholder": "https://api.openai.com/v1",
|
||||
"settings.speech.sttModel.title": "Modèle de transcription",
|
||||
|
|
@ -267,8 +284,8 @@ export const settingsMessages = {
|
|||
"settings.speech.ttsVoice.subtitle": "Voix de synthèse vocale par défaut réservée aux futures fonctions de lecture.",
|
||||
"settings.speech.playbackMode.title": "Mode de lecture",
|
||||
"settings.speech.playbackMode.subtitle": "Choisissez si le TTS commence a jouer pendant le flux audio ou apres la generation complete du fichier.",
|
||||
"settings.speech.playbackMode.streaming": "Streaming",
|
||||
"settings.speech.playbackMode.buffered": "Buffered",
|
||||
"settings.speech.playbackMode.streaming": "Flux continu",
|
||||
"settings.speech.playbackMode.buffered": "Mis en mémoire tampon",
|
||||
"settings.speech.ttsFormat.title": "Format de sortie",
|
||||
"settings.speech.ttsFormat.subtitle": "Choisissez le format audio pour la voix synthetisee. La prise en charge du streaming depend du fournisseur et du navigateur.",
|
||||
"settings.speech.help": "La saisie vocale du prompt apparait lorsque la transcription vocale est configuree et prise en charge. La lecture des messages utilise le mode et le format TTS selectionnes ici.",
|
||||
|
|
@ -286,41 +303,41 @@ export const settingsMessages = {
|
|||
"settings.speech.save.unsaved": "Modifications non enregistrées",
|
||||
"settings.speech.save.error": "Échec de l'enregistrement",
|
||||
"settings.nav.sidecars": "SideCars",
|
||||
"settings.section.sidecars.eyebrow": "Server services",
|
||||
"settings.section.sidecars.eyebrow": "Services serveur",
|
||||
"settings.section.sidecars.title": "SideCars",
|
||||
"settings.section.sidecars.subtitle": "Configure local services listening on fixed ports that the server can proxy into tabs.",
|
||||
"sidecars.form.name": "Name",
|
||||
"sidecars.form.validation": "Enter a valid SideCar name and port.",
|
||||
"settings.section.sidecars.subtitle": "Configurez les services locaux écoutant sur des ports fixes que le serveur peut relayer dans des onglets.",
|
||||
"sidecars.form.name": "Nom",
|
||||
"sidecars.form.validation": "Saisissez un nom et un port SideCar valides.",
|
||||
"sidecars.form.port": "Port",
|
||||
"sidecars.form.insecure": "Use HTTP",
|
||||
"sidecars.form.protocol": "Protocol",
|
||||
"sidecars.form.protocol.help": "Choose how the proxy should connect to the local service.",
|
||||
"sidecars.form.insecure": "Utiliser HTTP",
|
||||
"sidecars.form.protocol": "Protocole",
|
||||
"sidecars.form.protocol.help": "Choisissez comment le proxy doit se connecter au service local.",
|
||||
"sidecars.form.protocol.https": "HTTPS",
|
||||
"sidecars.form.protocol.http": "HTTP",
|
||||
"sidecars.form.prefixMode": "Prefix mode",
|
||||
"sidecars.form.prefixMode.help": "Choose whether the SideCar receives the public /sidecars path prefix.",
|
||||
"sidecars.form.prefixMode.strip": "Strip prefix",
|
||||
"sidecars.form.prefixMode.preserve": "Preserve prefix",
|
||||
"sidecars.form.add": "Add SideCar",
|
||||
"sidecars.form.prefixMode": "Mode de préfixe",
|
||||
"sidecars.form.prefixMode.help": "Choisissez si le SideCar reçoit le préfixe de chemin public /sidecars.",
|
||||
"sidecars.form.prefixMode.strip": "Retirer le préfixe",
|
||||
"sidecars.form.prefixMode.preserve": "Conserver le préfixe",
|
||||
"sidecars.form.add": "Ajouter un SideCar",
|
||||
"sidecars.kind.port": "Port",
|
||||
"sidecars.status.running": "Running",
|
||||
"sidecars.status.stopped": "Stopped",
|
||||
"sidecars.basePath": "Base path",
|
||||
"sidecars.settings.listTitle": "Configured SideCars",
|
||||
"sidecars.settings.listSubtitle": "Review the port-based SideCars available in the picker.",
|
||||
"sidecars.settings.empty": "No SideCars configured yet.",
|
||||
"sidecars.picker.title": "Open SideCar",
|
||||
"sidecars.picker.loading": "Loading SideCars...",
|
||||
"sidecars.picker.subtitle": "Choose an available SideCar to open in a new tab.",
|
||||
"sidecars.picker.empty": "No port-based SideCars are available yet.",
|
||||
"sidecars.picker.close": "Close",
|
||||
"sidecars.open.errorTitle": "Unable to open SideCar",
|
||||
"sidecars.open.notFound": "SideCar not found.",
|
||||
"sidecars.open.notRunning": "SideCar is not reachable on its configured port.",
|
||||
"sidecars.back": "Back",
|
||||
"sidecars.refresh": "Refresh",
|
||||
"sidecars.path": "Path",
|
||||
"sidecars.go": "Go",
|
||||
"sidecars.status.running": "En cours d'exécution",
|
||||
"sidecars.status.stopped": "Arrêté",
|
||||
"sidecars.basePath": "Chemin de base",
|
||||
"sidecars.settings.listTitle": "SideCars configurés",
|
||||
"sidecars.settings.listSubtitle": "Consultez les SideCars basés sur des ports disponibles dans le sélecteur.",
|
||||
"sidecars.settings.empty": "Aucun SideCar configuré pour le moment.",
|
||||
"sidecars.picker.title": "Ouvrir SideCar",
|
||||
"sidecars.picker.loading": "Chargement des SideCars...",
|
||||
"sidecars.picker.subtitle": "Choisissez un SideCar disponible à ouvrir dans un nouvel onglet.",
|
||||
"sidecars.picker.empty": "Aucun SideCar basé sur un port n'est disponible pour le moment.",
|
||||
"sidecars.picker.close": "Fermer",
|
||||
"sidecars.open.errorTitle": "Impossible d'ouvrir SideCar",
|
||||
"sidecars.open.notFound": "SideCar introuvable.",
|
||||
"sidecars.open.notRunning": "SideCar n'est pas joignable sur son port configuré.",
|
||||
"sidecars.back": "Retour",
|
||||
"sidecars.refresh": "Actualiser",
|
||||
"sidecars.path": "Chemin",
|
||||
"sidecars.go": "Aller",
|
||||
|
||||
// Toast History Panel
|
||||
"toastHistory.title": "Historique des notifications",
|
||||
|
|
@ -343,25 +360,25 @@ export const settingsMessages = {
|
|||
"toastHistory.deleteItem": "Supprimer la notification",
|
||||
|
||||
// Info Section
|
||||
"settings.section.info.title": "About",
|
||||
"settings.section.info.subtitle": "View version, runtime, and gather diagnostic information.",
|
||||
"settings.info.version.server": "Server version",
|
||||
"settings.info.version.ui": "UI version",
|
||||
"settings.info.version.uiSource": "UI source",
|
||||
"settings.info.runtime.type": "Runtime",
|
||||
"settings.info.runtime.platform": "Platform",
|
||||
"settings.info.runtime.os": "Operating system",
|
||||
"settings.info.server.url": "Server URL",
|
||||
"settings.info.server.root": "Workspace root",
|
||||
"settings.info.updates.title": "Updates",
|
||||
"settings.info.updates.subtitle": "Check for new releases of CodeNomad.",
|
||||
"settings.info.updates.latest": "Latest version",
|
||||
"settings.info.updates.download": "Download update",
|
||||
"settings.info.updates.refresh": "Refresh",
|
||||
"settings.info.diagnostics.copyFailed": "Clipboard access denied. Use the download button instead.",
|
||||
"settings.section.info.title": "À propos",
|
||||
"settings.section.info.subtitle": "Consultez la version et l'environnement d'exécution, et recueillez des informations de diagnostic.",
|
||||
"settings.info.version.server": "Version du serveur",
|
||||
"settings.info.version.ui": "Version de l'interface",
|
||||
"settings.info.version.uiSource": "Source de l'interface",
|
||||
"settings.info.runtime.type": "Environnement d'exécution",
|
||||
"settings.info.runtime.platform": "Plateforme",
|
||||
"settings.info.runtime.os": "Système d'exploitation",
|
||||
"settings.info.server.url": "URL du serveur",
|
||||
"settings.info.server.root": "Racine de l'espace de travail",
|
||||
"settings.info.updates.title": "Mises à jour",
|
||||
"settings.info.updates.subtitle": "Recherchez les nouvelles versions de CodeNomad.",
|
||||
"settings.info.updates.latest": "Dernière version",
|
||||
"settings.info.updates.download": "Télécharger la mise à jour",
|
||||
"settings.info.updates.refresh": "Actualiser",
|
||||
"settings.info.diagnostics.copyFailed": "Accès au presse-papiers refusé. Utilisez plutôt le bouton de téléchargement.",
|
||||
"settings.info.diagnostics.title": "Diagnostics",
|
||||
"settings.info.diagnostics.subtitle": "Collectez les informations système et de version pour les rapports de bug.",
|
||||
"settings.info.diagnostics.copy": "Copy to clipboard",
|
||||
"settings.info.diagnostics.download": "Download .txt",
|
||||
"settings.info.diagnostics.copied": "Diagnostic info copied to clipboard.",
|
||||
"settings.info.diagnostics.copy": "Copier dans le presse-papiers",
|
||||
"settings.info.diagnostics.download": "Télécharger .txt",
|
||||
"settings.info.diagnostics.copied": "Informations de diagnostic copiées dans le presse-papiers.",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -2,16 +2,22 @@ export const toolCallMessages = {
|
|||
"toolCall.pending.waitingToRun": "En attente d'exécution...",
|
||||
"toolCall.error.label": "Erreur :",
|
||||
|
||||
"toolCall.header.copyTitle": "Copy tool call title",
|
||||
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
||||
"toolCall.header.copyTitle": "Copier le titre de l'appel d'outil",
|
||||
"toolCall.header.copyAriaLabel": "Copier le titre de l'appel d'outil",
|
||||
"toolCall.header.copyOutputTitle": "Copier la sortie de l'outil",
|
||||
"toolCall.header.copyOutputAriaLabel": "Copier la sortie de l'outil",
|
||||
|
||||
"toolCall.header.showInputTitle": "Show Tool Arguments",
|
||||
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
|
||||
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
|
||||
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
|
||||
"toolCall.header.showInputTitle": "Afficher les arguments de l'outil",
|
||||
"toolCall.header.showInputAriaLabel": "Afficher les arguments de l'outil",
|
||||
"toolCall.header.hideInputTitle": "Masquer les arguments de l'outil",
|
||||
"toolCall.header.hideInputAriaLabel": "Masquer les arguments de l'outil",
|
||||
|
||||
"toolCall.io.input": "Tool Input",
|
||||
"toolCall.io.output": "Tool Output",
|
||||
"toolCall.io.input": "Entrée de l'outil",
|
||||
"toolCall.io.output": "Sortie de l'outil",
|
||||
"toolCall.io.copyInputTitle": "Copier l'entrée de l'outil",
|
||||
"toolCall.io.copyInputAriaLabel": "Copier l'entrée de l'outil",
|
||||
"toolCall.io.copyOutputTitle": "Copier la sortie de l'outil",
|
||||
"toolCall.io.copyOutputAriaLabel": "Copier la sortie de l'outil",
|
||||
|
||||
"toolCall.diff.label": "Diff",
|
||||
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||
|
|
@ -53,7 +59,7 @@ export const toolCallMessages = {
|
|||
"toolCall.renderer.read.detail.offset": "Décalage : {offset}",
|
||||
"toolCall.renderer.read.detail.limit": "Limite : {limit}",
|
||||
|
||||
"toolCall.renderer.todo.empty": "Aucun élément de plan pour l'instant.",
|
||||
"toolCall.renderer.todo.empty": "Aucun élément planifié",
|
||||
"toolCall.renderer.todo.status.pending": "En attente",
|
||||
"toolCall.renderer.todo.status.inProgress": "En cours",
|
||||
"toolCall.renderer.todo.status.completed": "Terminé",
|
||||
|
|
@ -71,13 +77,9 @@ export const toolCallMessages = {
|
|||
"toolCall.permission.actions.allowOnce": "Autoriser une fois",
|
||||
"toolCall.permission.actions.alwaysAllow": "Toujours autoriser",
|
||||
"toolCall.permission.actions.deny": "Refuser",
|
||||
"toolCall.permission.actions.confirmDeny": "Confirmer le refus",
|
||||
"toolCall.permission.actions.cancel": "Annuler",
|
||||
"toolCall.permission.shortcuts.allowOnce": "Autoriser une fois",
|
||||
"toolCall.permission.shortcuts.alwaysAllow": "Toujours autoriser",
|
||||
"toolCall.permission.rejectReason.label": "Pourquoi refuser cette autorisation ?",
|
||||
"toolCall.permission.rejectReason.placeholder": "Indiquez à CodeNomad quoi faire différemment...",
|
||||
"toolCall.permission.rejectReason.hint": "L'agent verra ce retour et pourra ajuster sa prochaine action.",
|
||||
"toolCall.permission.rejectReason.placeholder": "Retour facultatif pour Refuser, p. ex. utilise une commande plus sûre...",
|
||||
"toolCall.permission.errors.unableToUpdate": "Impossible de mettre à jour l'autorisation",
|
||||
|
||||
"permissionApproval.title": "Demandes",
|
||||
|
|
@ -86,7 +88,7 @@ export const toolCallMessages = {
|
|||
"permissionApproval.kind.question": "Question",
|
||||
"permissionApproval.questionCount.one": "{count} question",
|
||||
"permissionApproval.questionCount.other": "{count} questions",
|
||||
"permissionApproval.status.active": "Active",
|
||||
"permissionApproval.status.active": "Actif",
|
||||
"permissionApproval.actions.closeAriaLabel": "Fermer",
|
||||
"permissionApproval.actions.goToSession": "Aller à la session",
|
||||
"permissionApproval.actions.loadingSession": "Chargement…",
|
||||
|
|
@ -94,11 +96,7 @@ export const toolCallMessages = {
|
|||
"permissionApproval.actions.allowOnce": "Autoriser une fois",
|
||||
"permissionApproval.actions.alwaysAllow": "Toujours autoriser",
|
||||
"permissionApproval.actions.deny": "Refuser",
|
||||
"permissionApproval.actions.confirmDeny": "Confirmer le refus",
|
||||
"permissionApproval.actions.cancel": "Annuler",
|
||||
"permissionApproval.rejectReason.label": "Pourquoi refuser cette autorisation ?",
|
||||
"permissionApproval.rejectReason.placeholder": "Indiquez à CodeNomad quoi faire différemment...",
|
||||
"permissionApproval.rejectReason.hint": "L'agent verra ce retour et pourra ajuster sa prochaine action.",
|
||||
"permissionApproval.rejectReason.placeholder": "Retour facultatif pour Refuser, p. ex. utilise une commande plus sûre...",
|
||||
"permissionApproval.fallbackHint": "Chargez la session pour plus d'informations.",
|
||||
"permissionApproval.errors.unableToUpdatePermission": "Impossible de mettre à jour l'autorisation",
|
||||
|
||||
|
|
@ -111,7 +109,7 @@ export const toolCallMessages = {
|
|||
"toolCall.question.type.one": "Question",
|
||||
"toolCall.question.type.other": "Questions",
|
||||
"toolCall.question.number": "Q{number}:",
|
||||
"toolCall.question.multiple": "Multiple",
|
||||
"toolCall.question.multiple": "Multiple choix",
|
||||
"toolCall.question.custom.title": "Saisir une réponse personnalisée",
|
||||
"toolCall.question.custom.label": "Réponse personnalisée",
|
||||
"toolCall.question.custom.placeholder": "Saisissez votre propre réponse",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const appMessages = {
|
|||
"app.stopInstance.title": "עצור מופע",
|
||||
"app.stopInstance.confirmLabel": "עצור",
|
||||
"app.stopInstance.cancelLabel": "המשך להריץ",
|
||||
"app.stopInstance.toast.error": "עצירת סביבת העבודה נכשלה.",
|
||||
|
||||
"emptyState.logoAlt": "לוגו CodeNomad",
|
||||
"emptyState.brandTitle": "CodeNomad",
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.certificateInstall.confirmLabel": "המשך",
|
||||
"folderSelection.servers.certificateInstall.cancelLabel": "ביטול",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "CodeNomad צריך שהאישור המקומי יהיה מהימן לפני שיוכל לפתוח חלונות HTTPS מרוחקים עם אישור בחתימה עצמית.",
|
||||
"folderSelection.sidecars.button": "Open SideCar",
|
||||
"folderSelection.sidecars.button": "פתח SideCar",
|
||||
|
||||
"projectRenameDialog.title": "שנה שם סביבת עבודה",
|
||||
"projectRenameDialog.description.withLabel": "עדכן את שם סביבת העבודה עבור \"{label}\".",
|
||||
|
|
|
|||
|
|
@ -119,6 +119,22 @@ export const instanceMessages = {
|
|||
"instanceShell.rightPanel.sections.plugins": "תוספים",
|
||||
"instanceShell.rightPanel.sections.plugins.tooltip": "תוספים המתאימים אישית את הממשק ואת התנהגות השרת, ומוסיפים תכונות מעבר ל-MCP ו-LSP.",
|
||||
|
||||
"instanceShell.gitChanges.noSessionSelected": "בחר סשן לצפייה בשינויי Git.",
|
||||
"instanceShell.gitChanges.loading": "טוען שינויי Git…",
|
||||
"instanceShell.gitChanges.empty": "אין שינויי Git עדיין.",
|
||||
"instanceShell.gitChanges.deleted": "נמחק",
|
||||
"instanceShell.gitChanges.binaryViewer": "לא ניתן להציג קובץ בינארי",
|
||||
"instanceShell.gitChanges.sections.staged": "שינויים שנשמרו ל-staging",
|
||||
"instanceShell.gitChanges.sections.unstaged": "שינויים",
|
||||
"instanceShell.gitChanges.actions.insertContext": "הוסף לפרומפט",
|
||||
"instanceShell.gitChanges.actions.stage": "העבר ל-staging",
|
||||
"instanceShell.gitChanges.actions.unstage": "הוצא מ-staging",
|
||||
"instanceShell.gitChanges.commit.placeholder": "הזן הודעת commit",
|
||||
"instanceShell.gitChanges.commit.submit": "בצע commit",
|
||||
"instanceShell.gitChanges.commit.submitting": "מבצע commit...",
|
||||
"instanceShell.gitChanges.commit.success": "ה-commit נוצר בהצלחה",
|
||||
"instanceShell.gitChanges.commit.error": "יצירת ה-commit נכשלה",
|
||||
|
||||
"instanceShell.filesShell.fileListTitle": "רשימת קבצים",
|
||||
"instanceShell.filesShell.mobileSelectorLabel": "בחר קובץ",
|
||||
"instanceShell.filesShell.mobileSelectorEmpty": "בחר קובץ",
|
||||
|
|
@ -138,21 +154,6 @@ export const instanceMessages = {
|
|||
"instanceShell.filesShell.showSource": "הצג מקור",
|
||||
"instanceShell.filesShell.enableWordWrap": "הפעל גלישת מילים",
|
||||
"instanceShell.filesShell.disableWordWrap": "כבה גלישת מילים",
|
||||
"instanceShell.gitChanges.noSessionSelected": "בחר סשן לצפייה בשינויי Git.",
|
||||
"instanceShell.gitChanges.loading": "טוען שינויי Git…",
|
||||
"instanceShell.gitChanges.empty": "אין שינויי Git עדיין.",
|
||||
"instanceShell.gitChanges.deleted": "נמחק",
|
||||
"instanceShell.gitChanges.binaryViewer": "לא ניתן להציג קובץ בינארי",
|
||||
"instanceShell.gitChanges.sections.staged": "שינויים שנשמרו ל-staging",
|
||||
"instanceShell.gitChanges.sections.unstaged": "שינויים",
|
||||
"instanceShell.gitChanges.actions.insertContext": "הוסף לפרומפט",
|
||||
"instanceShell.gitChanges.actions.stage": "העבר ל-staging",
|
||||
"instanceShell.gitChanges.actions.unstage": "הוצא מ-staging",
|
||||
"instanceShell.gitChanges.commit.placeholder": "הזן הודעת commit",
|
||||
"instanceShell.gitChanges.commit.submit": "Commit",
|
||||
"instanceShell.gitChanges.commit.submitting": "מבצע commit...",
|
||||
"instanceShell.gitChanges.commit.success": "ה-commit נוצר בהצלחה",
|
||||
"instanceShell.gitChanges.commit.error": "יצירת ה-commit נכשלה",
|
||||
"instanceShell.diff.hideUnchanged": "הסתר אזורים ללא שינוי",
|
||||
"instanceShell.diff.showFull": "הצג קובץ מלא",
|
||||
"instanceShell.diff.switchToSplit": "עבור לתצוגה מפוצלת",
|
||||
|
|
@ -160,24 +161,6 @@ export const instanceMessages = {
|
|||
"instanceShell.diff.enableWordWrap": "הפעל גלישת מילים",
|
||||
"instanceShell.diff.disableWordWrap": "כבה גלישת מילים",
|
||||
"instanceShell.worktree.create": "+ צור worktree",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "בחר סשן לצפייה בתוכנית.",
|
||||
"instanceShell.plan.empty": "עדיין לא תוכנן דבר.",
|
||||
|
||||
"instanceShell.yoloMode.noSessionSelected": "בחרו סשן כדי להגדיר מצב Yolo.",
|
||||
"instanceShell.yoloMode.title": "מצב Yolo",
|
||||
"instanceShell.yoloMode.description": "מאשר אוטומטית בקשות הרשאה עבור הסשן הזה. כבוי כברירת מחדל.",
|
||||
"instanceShell.yoloMode.badge": "Yolo",
|
||||
"instanceShell.yoloMode.badgeAriaLabel": "מצב Yolo פעיל",
|
||||
|
||||
"instanceShell.backgroundProcesses.empty": "אין תהליכי רקע.",
|
||||
"instanceShell.backgroundProcesses.status": "סטטוס: {status}",
|
||||
"instanceShell.backgroundProcesses.output": "פלט: {sizeKb}KB",
|
||||
"instanceShell.backgroundProcesses.notify.enabled": "התראת סיום פעילה",
|
||||
"instanceShell.backgroundProcesses.notify.disabled": "התראת סיום כבויה",
|
||||
"instanceShell.backgroundProcesses.actions.output": "פלט",
|
||||
"instanceShell.backgroundProcesses.actions.stop": "עצור",
|
||||
"instanceShell.backgroundProcesses.actions.terminate": "סיים",
|
||||
"instanceShell.worktree.delete.error.title": "המחיקה נכשלה",
|
||||
"instanceShell.worktree.delete.error.fallback": "מחיקת ה-worktree נכשלה",
|
||||
"instanceShell.worktree.delete.error.causeLabel": "סיבה סבירה:",
|
||||
|
|
@ -203,6 +186,24 @@ export const instanceMessages = {
|
|||
"instanceShell.worktree.delete.error.copySanitizedSuccess": "שגיאת המחיקה המסוננת הועתקה",
|
||||
"instanceShell.worktree.delete.error.copyFailure": "העתקת שגיאת המחיקה נכשלה",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "בחר סשן לצפייה בתוכנית.",
|
||||
"instanceShell.plan.empty": "עדיין לא תוכנן דבר.",
|
||||
|
||||
"instanceShell.yoloMode.noSessionSelected": "בחרו סשן כדי להגדיר מצב Yolo.",
|
||||
"instanceShell.yoloMode.title": "מצב Yolo",
|
||||
"instanceShell.yoloMode.description": "מאשר אוטומטית בקשות הרשאה עבור הסשן הזה. כבוי כברירת מחדל.",
|
||||
"instanceShell.yoloMode.badge": "Yolo",
|
||||
"instanceShell.yoloMode.badgeAriaLabel": "מצב Yolo פעיל",
|
||||
|
||||
"instanceShell.backgroundProcesses.empty": "אין תהליכי רקע.",
|
||||
"instanceShell.backgroundProcesses.status": "סטטוס: {status}",
|
||||
"instanceShell.backgroundProcesses.output": "פלט: {sizeKb}KB",
|
||||
"instanceShell.backgroundProcesses.notify.enabled": "התראת סיום פעילה",
|
||||
"instanceShell.backgroundProcesses.notify.disabled": "התראת סיום כבויה",
|
||||
"instanceShell.backgroundProcesses.actions.output": "פלט",
|
||||
"instanceShell.backgroundProcesses.actions.stop": "עצור",
|
||||
"instanceShell.backgroundProcesses.actions.terminate": "סיים",
|
||||
|
||||
"versionPill.appWithVersion": "אפליקציה {version}",
|
||||
"versionPill.ui": "ממשק",
|
||||
"versionPill.uiWithVersion": "ממשק {version}",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const messagingMessages = {
|
|||
"messageSection.loadError.reload": "טען הודעות מחדש",
|
||||
"messageSection.scroll.toFirstAriaLabel": "גלול להודעה הראשונה",
|
||||
"messageSection.scroll.toLatestAriaLabel": "גלול להודעה האחרונה",
|
||||
"messageSection.scroll.showControlsAriaLabel": "הצג פקדי גלילה",
|
||||
"messageSection.scroll.enableHoldAriaLabel": "הפעל עצירה לתגובות עוזר ארוכות",
|
||||
"messageSection.scroll.disableHoldAriaLabel": "כבה עצירה לתגובות עוזר ארוכות",
|
||||
"messageSection.quote.addAsQuote": "הוסף כציטוט",
|
||||
|
|
@ -78,6 +79,15 @@ export const messagingMessages = {
|
|||
"messageBlock.reasoning.indicator.hide": "הסתר",
|
||||
"messageBlock.reasoning.indicator.view": "צפה",
|
||||
"messageBlock.reasoning.detailsAriaLabel": "פרטי חשיבה",
|
||||
"messageBlock.reasoning.thoughtsFallback": "מחשבות",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.one": "חשב במשך {count} שנייה",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.other": "חשב במשך {count} שניות",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.one": "חשב במשך {count} דקה",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.other": "חשב במשך {count} דקות",
|
||||
"messageBlock.reasoning.thoughtFor.hours.one": "חשב במשך {count} שעה",
|
||||
"messageBlock.reasoning.thoughtFor.hours.other": "חשב במשך {count} שעות",
|
||||
"messageBlock.reasoning.copyTitle": "העתק מחשבות",
|
||||
"messageBlock.reasoning.copyAriaLabel": "העתק מחשבות",
|
||||
|
||||
"codeBlockInline.actions.copy": "העתק",
|
||||
"codeBlockInline.actions.copied": "הועתק!",
|
||||
|
|
@ -106,6 +116,14 @@ export const messagingMessages = {
|
|||
"messageItem.actions.deleteMessageFailedMessage": "מחיקת ההודעה נכשלה",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "בחר הודעה למחיקה",
|
||||
"messageItem.selection.selectForDeletion": "בחר הודעה למחיקה",
|
||||
"messageItem.selection.deselectForDeletion": "בטל בחירת הודעה למחיקה",
|
||||
"messageItem.duration.workedFor.seconds.one": "עבד במשך {count} שנייה",
|
||||
"messageItem.duration.workedFor.seconds.other": "עבד במשך {count} שניות",
|
||||
"messageItem.duration.workedFor.minutes.one": "עבד במשך {count} דקה",
|
||||
"messageItem.duration.workedFor.minutes.other": "עבד במשך {count} דקות",
|
||||
"messageItem.duration.workedFor.hours.one": "עבד במשך {count} שעה",
|
||||
"messageItem.duration.workedFor.hours.other": "עבד במשך {count} שעות",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "פריטים נבחרים ({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "מחק פריטים נבחרים",
|
||||
|
|
@ -153,7 +171,6 @@ export const messagingMessages = {
|
|||
"promptInput.hints.commands": "פקודות",
|
||||
"promptInput.history.previousAriaLabel": "פקודה קודמת",
|
||||
"promptInput.history.nextAriaLabel": "פקודה הבאה",
|
||||
"promptInput.resizeHandle.title": "גרור כדי לשנות את גובה שדה הקלט",
|
||||
"promptInput.overlay.newLine": "שורה חדשה",
|
||||
"promptInput.overlay.send": "שלח",
|
||||
"promptInput.overlay.filesAgents": "קבצים/סוכנים",
|
||||
|
|
@ -189,4 +206,5 @@ export const messagingMessages = {
|
|||
"promptInput.voiceInput.error.permissionDenied": "הגישה למיקרופון נדחתה על ידי macOS.",
|
||||
"promptInput.voiceInput.error.unsupported": "קלט קולי אינו נתמך בדפדפן זה.",
|
||||
"promptInput.voiceInput.error.transcribe": "לא ניתן היה לתמלל את האודיו שהוקלט.",
|
||||
"promptInput.resizeHandle.title": "גרור כדי לשנות את גובה שדה הקלט",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const settingsMessages = {
|
|||
"modelSelector.favoritesOnly.showAll": "הצג את כל המודלים",
|
||||
"modelSelector.favorite.add": "הוסף למועדפים",
|
||||
"modelSelector.favorite.remove": "הסר ממועדפים",
|
||||
"modelSelector.manageProviders": "Manage Providers",
|
||||
"modelSelector.manageProviders": "נהל ספקים",
|
||||
|
||||
"thinkingSelector.variant.default": "ברירת מחדל",
|
||||
"thinkingSelector.label": "חשיבה: {variant}",
|
||||
|
|
@ -74,10 +74,10 @@ export const settingsMessages = {
|
|||
"settings.nav.notifications": "התראות",
|
||||
"settings.nav.remote": "גישה מרוחקת",
|
||||
"settings.nav.speech": "קול",
|
||||
"settings.nav.providers": "Providers",
|
||||
"settings.nav.providers": "ספקים",
|
||||
"settings.nav.opencode": "OpenCode",
|
||||
"settings.nav.configFiles": "Config Files",
|
||||
"settings.nav.info": "Info",
|
||||
"settings.nav.configFiles": "קובצי הגדרות",
|
||||
"settings.nav.info": "מידע",
|
||||
"settings.scope.device": "מכשיר זה",
|
||||
"settings.scope.server": "הגדרת שרת",
|
||||
"settings.common.enabled": "מופעל",
|
||||
|
|
@ -132,83 +132,100 @@ export const settingsMessages = {
|
|||
"settings.opencode.logLevel.option.info": "מידע",
|
||||
"settings.opencode.logLevel.option.warn": "אזהרה",
|
||||
"settings.opencode.logLevel.option.error": "שגיאה",
|
||||
"settings.configFiles.title": "Config Files",
|
||||
"settings.configFiles.subtitle": "Edit allowlisted global configuration files on this server.",
|
||||
"settings.configFiles.selector.label": "File",
|
||||
"settings.configFiles.selector.placeholder": "Select config file",
|
||||
"settings.configFiles.actions.save": "Save",
|
||||
"settings.configFiles.actions.reload": "Reload",
|
||||
"settings.configFiles.state.unsaved": "Unsaved changes",
|
||||
"settings.configFiles.state.notCreated": "File will be created on save",
|
||||
"settings.configFiles.empty": "No config file selected.",
|
||||
"settings.configFiles.errors.loadList": "Failed to load config files.",
|
||||
"settings.configFiles.errors.loadContent": "Failed to load config file.",
|
||||
"settings.configFiles.errors.save": "Failed to save config file.",
|
||||
"settings.configFiles.toast.saveSuccess": "Config file saved.",
|
||||
"settings.configFiles.toast.saveError": "Failed to save config file.",
|
||||
"settings.configFiles.confirmDiscard.message": "Discard unsaved changes before continuing?",
|
||||
"settings.configFiles.confirmDiscard.confirmLabel": "Discard changes",
|
||||
"settings.configFiles.confirmDiscard.cancelLabel": "Keep editing",
|
||||
"settings.configFiles.title": "קובצי הגדרות",
|
||||
"settings.configFiles.subtitle": "ערוך קובצי הגדרות גלובליים שנמצאים ברשימת ההרשאות בשרת הזה.",
|
||||
"settings.configFiles.selector.label": "קובץ",
|
||||
"settings.configFiles.selector.placeholder": "בחר קובץ הגדרות",
|
||||
"settings.configFiles.actions.save": "שמור",
|
||||
"settings.configFiles.actions.reload": "טען מחדש",
|
||||
"settings.configFiles.state.unsaved": "שינויים שלא נשמרו",
|
||||
"settings.configFiles.state.notCreated": "הקובץ ייווצר בעת השמירה",
|
||||
"settings.configFiles.empty": "לא נבחר קובץ הגדרות.",
|
||||
"settings.configFiles.errors.loadList": "טעינת קובצי ההגדרות נכשלה.",
|
||||
"settings.configFiles.errors.loadContent": "טעינת קובץ ההגדרות נכשלה.",
|
||||
"settings.configFiles.errors.save": "שמירת קובץ ההגדרות נכשלה.",
|
||||
"settings.configFiles.toast.saveSuccess": "קובץ ההגדרות נשמר.",
|
||||
"settings.configFiles.toast.saveError": "שמירת קובץ ההגדרות נכשלה.",
|
||||
"settings.configFiles.confirmDiscard.message": "לזרוק שינויים שלא נשמרו לפני שממשיכים?",
|
||||
"settings.configFiles.confirmDiscard.confirmLabel": "זרוק שינויים",
|
||||
"settings.configFiles.confirmDiscard.cancelLabel": "המשך לערוך",
|
||||
|
||||
"settings.providers.title": "Provider Authentication",
|
||||
"settings.providers.subtitle": "Connect API keys or OAuth accounts for the active OpenCode instance.",
|
||||
"settings.providers.refresh": "Refresh",
|
||||
"settings.providers.selectProvider": "Select provider",
|
||||
"settings.providers.available.title": "Available Providers",
|
||||
"settings.providers.configured.title": "Configured Providers",
|
||||
"settings.providers.loading": "Loading provider authentication methods...",
|
||||
"settings.providers.empty.noInstance": "Start an OpenCode workspace before managing provider authentication.",
|
||||
"settings.providers.empty.noProviders": "No providers are available from this OpenCode instance.",
|
||||
"settings.providers.empty.noConfiguredProviders": "No providers are currently configured for this OpenCode instance.",
|
||||
"settings.providers.models.one": "{count} model",
|
||||
"settings.providers.models.other": "{count} models",
|
||||
"settings.providers.actions.connect": "Connect",
|
||||
"settings.providers.actions.disconnect": "Disconnect",
|
||||
"settings.providers.actions.remove": "Remove saved auth",
|
||||
"settings.providers.actions.close": "Close",
|
||||
"settings.providers.actions.continue": "Continue",
|
||||
"settings.providers.actions.working": "Working...",
|
||||
"settings.providers.method.title": "Authentication method",
|
||||
"settings.providers.method.subtitle": "Choose the flow OpenCode exposes for this provider.",
|
||||
"settings.providers.method.api": "API key",
|
||||
"settings.providers.title": "אימות ספקים",
|
||||
"settings.providers.subtitle": "חבר מפתחות API או חשבונות OAuth עבור מופע OpenCode הפעיל.",
|
||||
"settings.providers.refresh": "רענן",
|
||||
"settings.providers.selectProvider": "בחר ספק",
|
||||
"settings.providers.available.title": "ספקים זמינים",
|
||||
"settings.providers.configured.title": "ספקים מוגדרים",
|
||||
"settings.providers.loading": "טוען שיטות אימות ספקים...",
|
||||
"settings.providers.empty.noInstance": "הפעל סביבת עבודה של OpenCode לפני ניהול אימות ספקים.",
|
||||
"settings.providers.empty.noProviders": "אין ספקים זמינים ממופע OpenCode הזה.",
|
||||
"settings.providers.empty.noConfiguredProviders": "אין כרגע ספקים מוגדרים עבור מופע OpenCode הזה.",
|
||||
"settings.providers.models.one": "{count} מודל",
|
||||
"settings.providers.models.other": "{count} מודלים",
|
||||
"settings.providers.actions.connect": "התחבר",
|
||||
"settings.providers.actions.disconnect": "נתק",
|
||||
"settings.providers.actions.remove": "הסר אימות שמור",
|
||||
"settings.providers.actions.close": "סגור",
|
||||
"settings.providers.actions.continue": "המשך",
|
||||
"settings.providers.actions.working": "עובד...",
|
||||
"settings.providers.method.title": "שיטת אימות",
|
||||
"settings.providers.method.subtitle": "בחר את תהליך האימות ש-OpenCode חושף עבור הספק הזה.",
|
||||
"settings.providers.method.api": "מפתח API",
|
||||
"settings.providers.method.oauth": "OAuth",
|
||||
"settings.providers.method.mixed": "API key and OAuth",
|
||||
"settings.providers.method.fallback": "API key",
|
||||
"settings.providers.status.available": "Available",
|
||||
"settings.providers.auth.title": "Connect {provider}",
|
||||
"settings.providers.auth.subtitle": "Authentication is saved inside the active OpenCode server.",
|
||||
"settings.providers.apiKey.label": "API key",
|
||||
"settings.providers.apiKey.placeholder": "Paste provider API key",
|
||||
"settings.providers.oauth.noPrompts": "This OAuth method does not need additional details before opening the browser.",
|
||||
"settings.providers.oauth.promptsUnsupported": "This OAuth method requires provider-specific prompt inputs that are not supported by the current SDK build.",
|
||||
"settings.providers.oauth.enterCode": "Complete the browser flow, then paste the authorization code.",
|
||||
"settings.providers.oauth.openPage": "Open authorization page",
|
||||
"settings.providers.oauth.openHere": "Open in this tab",
|
||||
"settings.providers.oauth.copyLink": "Copy link",
|
||||
"settings.providers.oauth.linkCopied": "Link copied",
|
||||
"settings.providers.oauth.popupBlocked": "Your browser may have blocked the authorization page. Use the actions above to continue the sign-in flow.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "OpenCode authorization",
|
||||
"settings.providers.oauth.popup.loadingBody": "Preparing authorization page...",
|
||||
"settings.providers.oauth.codeLabel": "Authorization code",
|
||||
"settings.providers.oauth.codePlaceholder": "Paste code from provider",
|
||||
"settings.providers.oauth.submitCode": "Submit code",
|
||||
"settings.providers.oauth.waitingTitle": "Waiting for authorization",
|
||||
"settings.providers.oauth.cancelWait": "Cancel",
|
||||
"settings.providers.prompt.selectPlaceholder": "Select an option",
|
||||
"settings.providers.success": "Provider authentication saved. Provider data has been refreshed.",
|
||||
"settings.providers.source.api": "Saved provider authentication",
|
||||
"settings.providers.source.config": "Configured in opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configured by custom provider logic",
|
||||
"settings.providers.source.env": "Configured from environment variables",
|
||||
"settings.providers.source.unknown": "Configured provider source",
|
||||
"settings.providers.errors.loadFailed": "Failed to load provider authentication methods.",
|
||||
"settings.providers.errors.noAuthorization": "Provider did not return authorization data.",
|
||||
"settings.providers.errors.authorizationFailed": "Authorization failed.",
|
||||
"settings.providers.errors.removeFailed": "Failed to remove provider authentication.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "This provider is configured from environment variables and cannot be disconnected from the UI.",
|
||||
"settings.providers.method.mixed": "מפתח API ו-OAuth",
|
||||
"settings.providers.method.fallback": "מפתח API",
|
||||
"settings.providers.status.available": "זמין",
|
||||
"settings.providers.auth.title": "התחבר אל {provider}",
|
||||
"settings.providers.auth.subtitle": "האימות נשמר בתוך שרת OpenCode הפעיל.",
|
||||
"settings.providers.apiKey.label": "מפתח API",
|
||||
"settings.providers.apiKey.placeholder": "הדבק מפתח API של הספק",
|
||||
"settings.providers.oauth.noPrompts": "שיטת OAuth זו אינה דורשת פרטים נוספים לפני פתיחת הדפדפן.",
|
||||
"settings.providers.oauth.promptsUnsupported": "שיטת OAuth זו דורשת קלטים ייעודיים לספק שאינם נתמכים בגרסת ה-SDK הנוכחית.",
|
||||
"settings.providers.oauth.enterCode": "השלם את התהליך בדפדפן, ואז הדבק את קוד ההרשאה.",
|
||||
"settings.providers.oauth.openPage": "פתח דף הרשאה",
|
||||
"settings.providers.oauth.openHere": "פתח בלשונית זו",
|
||||
"settings.providers.oauth.copyLink": "העתק קישור",
|
||||
"settings.providers.oauth.linkCopied": "הקישור הועתק",
|
||||
"settings.providers.oauth.popupBlocked": "ייתכן שהדפדפן חסם את דף ההרשאה. השתמש בפעולות למעלה כדי להמשיך בתהליך ההתחברות.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "הרשאת OpenCode",
|
||||
"settings.providers.oauth.popup.loadingBody": "מכין דף הרשאה...",
|
||||
"settings.providers.oauth.codeLabel": "קוד הרשאה",
|
||||
"settings.providers.oauth.codePlaceholder": "הדבק קוד מהספק",
|
||||
"settings.providers.oauth.submitCode": "שלח קוד",
|
||||
"settings.providers.oauth.waitingTitle": "ממתין להרשאה",
|
||||
"settings.providers.oauth.cancelWait": "ביטול",
|
||||
"settings.providers.prompt.selectPlaceholder": "בחר אפשרות",
|
||||
"settings.providers.success": "אימות הספק נשמר. נתוני הספק רועננו.",
|
||||
"settings.providers.source.api": "אימות ספק שמור",
|
||||
"settings.providers.source.config": "מוגדר ב-opencode.jsonc",
|
||||
"settings.providers.source.custom": "מוגדר על ידי לוגיקת ספק מותאמת אישית",
|
||||
"settings.providers.source.env": "מוגדר ממשתני סביבה",
|
||||
"settings.providers.source.unknown": "מקור ספק מוגדר",
|
||||
"settings.providers.errors.loadFailed": "טעינת שיטות אימות הספקים נכשלה.",
|
||||
"settings.providers.errors.noAuthorization": "הספק לא החזיר נתוני הרשאה.",
|
||||
"settings.providers.errors.authorizationFailed": "ההרשאה נכשלה.",
|
||||
"settings.providers.errors.removeFailed": "הסרת אימות הספק נכשלה.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "ספק זה מוגדר ממשתני סביבה ולא ניתן לנתק אותו מהממשק.",
|
||||
|
||||
"settings.appearance.behavior.title": "אינטראקציה",
|
||||
"settings.appearance.behavior.subtitle": "ברירות מחדל להודעות, diff וקלט.",
|
||||
"settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets",
|
||||
"settings.behavior.expansionPreset.minimal.title": "Minimal",
|
||||
"settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.",
|
||||
"settings.behavior.expansionPreset.balanced.title": "Balanced",
|
||||
"settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.",
|
||||
"settings.behavior.expansionPreset.detailed.title": "Detailed",
|
||||
"settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.",
|
||||
"settings.behavior.expansionPreset.everything.title": "Everything",
|
||||
"settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.",
|
||||
"settings.behavior.expansionPreset.custom.title": "Custom",
|
||||
"settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.",
|
||||
"settings.behavior.expansionDefaults.title": "Expansion defaults",
|
||||
"settings.behavior.expansionDefaults.itemColumn": "Item",
|
||||
"settings.behavior.expansionDefaults.stateColumn": "Default state",
|
||||
"settings.behavior.expansionDefaults.thinking": "thinking",
|
||||
"settings.behavior.expansionDefaults.otherTools": "Other tools",
|
||||
"settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}",
|
||||
"settings.behavior.keyboardHints.title": "רמזי קיצורי מקלדת",
|
||||
"settings.behavior.keyboardHints.subtitle": "הצג רמזי קיצורי מקלדת בכל הממשק.",
|
||||
"settings.behavior.thinking.title": "קטעי חשיבה",
|
||||
|
|
@ -286,41 +303,41 @@ export const settingsMessages = {
|
|||
"settings.speech.save.unsaved": "יש שינויים שלא נשמרו",
|
||||
"settings.speech.save.error": "השמירה נכשלה",
|
||||
"settings.nav.sidecars": "SideCars",
|
||||
"settings.section.sidecars.eyebrow": "Server services",
|
||||
"settings.section.sidecars.eyebrow": "שירותי שרת",
|
||||
"settings.section.sidecars.title": "SideCars",
|
||||
"settings.section.sidecars.subtitle": "Configure local services listening on fixed ports that the server can proxy into tabs.",
|
||||
"sidecars.form.name": "Name",
|
||||
"sidecars.form.validation": "Enter a valid SideCar name and port.",
|
||||
"sidecars.form.port": "Port",
|
||||
"sidecars.form.insecure": "Use HTTP",
|
||||
"sidecars.form.protocol": "Protocol",
|
||||
"sidecars.form.protocol.help": "Choose how the proxy should connect to the local service.",
|
||||
"settings.section.sidecars.subtitle": "הגדר שירותים מקומיים שמאזינים בפורטים קבועים, כדי שהשרת יוכל להעביר אותם ללשוניות דרך proxy.",
|
||||
"sidecars.form.name": "שם",
|
||||
"sidecars.form.validation": "הזן שם SideCar ופורט תקינים.",
|
||||
"sidecars.form.port": "פורט",
|
||||
"sidecars.form.insecure": "השתמש ב-HTTP",
|
||||
"sidecars.form.protocol": "פרוטוקול",
|
||||
"sidecars.form.protocol.help": "בחר כיצד ה-proxy יתחבר לשירות המקומי.",
|
||||
"sidecars.form.protocol.https": "HTTPS",
|
||||
"sidecars.form.protocol.http": "HTTP",
|
||||
"sidecars.form.prefixMode": "Prefix mode",
|
||||
"sidecars.form.prefixMode.help": "Choose whether the SideCar receives the public /sidecars path prefix.",
|
||||
"sidecars.form.prefixMode.strip": "Strip prefix",
|
||||
"sidecars.form.prefixMode.preserve": "Preserve prefix",
|
||||
"sidecars.form.add": "Add SideCar",
|
||||
"sidecars.kind.port": "Port",
|
||||
"sidecars.status.running": "Running",
|
||||
"sidecars.status.stopped": "Stopped",
|
||||
"sidecars.basePath": "Base path",
|
||||
"sidecars.settings.listTitle": "Configured SideCars",
|
||||
"sidecars.settings.listSubtitle": "Review the port-based SideCars available in the picker.",
|
||||
"sidecars.settings.empty": "No SideCars configured yet.",
|
||||
"sidecars.picker.title": "Open SideCar",
|
||||
"sidecars.picker.loading": "Loading SideCars...",
|
||||
"sidecars.picker.subtitle": "Choose an available SideCar to open in a new tab.",
|
||||
"sidecars.picker.empty": "No port-based SideCars are available yet.",
|
||||
"sidecars.picker.close": "Close",
|
||||
"sidecars.open.errorTitle": "Unable to open SideCar",
|
||||
"sidecars.open.notFound": "SideCar not found.",
|
||||
"sidecars.open.notRunning": "SideCar is not reachable on its configured port.",
|
||||
"sidecars.back": "Back",
|
||||
"sidecars.refresh": "Refresh",
|
||||
"sidecars.path": "Path",
|
||||
"sidecars.go": "Go",
|
||||
"sidecars.form.prefixMode": "מצב קידומת",
|
||||
"sidecars.form.prefixMode.help": "בחר אם ה-SideCar יקבל את קידומת הנתיב הציבורית /sidecars.",
|
||||
"sidecars.form.prefixMode.strip": "הסר קידומת",
|
||||
"sidecars.form.prefixMode.preserve": "שמור קידומת",
|
||||
"sidecars.form.add": "הוסף SideCar",
|
||||
"sidecars.kind.port": "פורט",
|
||||
"sidecars.status.running": "רץ",
|
||||
"sidecars.status.stopped": "עצור",
|
||||
"sidecars.basePath": "נתיב בסיס",
|
||||
"sidecars.settings.listTitle": "SideCars מוגדרים",
|
||||
"sidecars.settings.listSubtitle": "סקור את ה-SideCars מבוססי הפורט הזמינים בבורר.",
|
||||
"sidecars.settings.empty": "עדיין לא הוגדרו SideCars.",
|
||||
"sidecars.picker.title": "פתח SideCar",
|
||||
"sidecars.picker.loading": "טוען SideCars...",
|
||||
"sidecars.picker.subtitle": "בחר SideCar זמין לפתיחה בלשונית חדשה.",
|
||||
"sidecars.picker.empty": "עדיין אין SideCars מבוססי פורט זמינים.",
|
||||
"sidecars.picker.close": "סגור",
|
||||
"sidecars.open.errorTitle": "לא ניתן לפתוח SideCar",
|
||||
"sidecars.open.notFound": "SideCar לא נמצא.",
|
||||
"sidecars.open.notRunning": "לא ניתן להגיע ל-SideCar בפורט שהוגדר לו.",
|
||||
"sidecars.back": "חזרה",
|
||||
"sidecars.refresh": "רענן",
|
||||
"sidecars.path": "נתיב",
|
||||
"sidecars.go": "עבור",
|
||||
|
||||
// Toast History Panel
|
||||
"toastHistory.title": "היסטוריית התראות",
|
||||
|
|
@ -343,25 +360,25 @@ export const settingsMessages = {
|
|||
"toastHistory.deleteItem": "מחק התראה",
|
||||
|
||||
// Info Section
|
||||
"settings.section.info.title": "About",
|
||||
"settings.section.info.subtitle": "View version, runtime, and gather diagnostic information.",
|
||||
"settings.info.version.server": "Server version",
|
||||
"settings.info.version.ui": "UI version",
|
||||
"settings.info.version.uiSource": "UI source",
|
||||
"settings.info.runtime.type": "Runtime",
|
||||
"settings.info.runtime.platform": "Platform",
|
||||
"settings.info.runtime.os": "Operating system",
|
||||
"settings.info.server.url": "Server URL",
|
||||
"settings.info.server.root": "Workspace root",
|
||||
"settings.info.updates.title": "Updates",
|
||||
"settings.info.updates.subtitle": "Check for new releases of CodeNomad.",
|
||||
"settings.info.updates.latest": "Latest version",
|
||||
"settings.info.updates.download": "Download update",
|
||||
"settings.info.updates.refresh": "Refresh",
|
||||
"settings.info.diagnostics.copyFailed": "Clipboard access denied. Use the download button instead.",
|
||||
"settings.info.diagnostics.title": "Diagnostics",
|
||||
"settings.section.info.title": "אודות",
|
||||
"settings.section.info.subtitle": "צפה בגרסה ובסביבת הריצה, ואסוף מידע אבחון.",
|
||||
"settings.info.version.server": "גרסת שרת",
|
||||
"settings.info.version.ui": "גרסת ממשק",
|
||||
"settings.info.version.uiSource": "מקור הממשק",
|
||||
"settings.info.runtime.type": "סביבת ריצה",
|
||||
"settings.info.runtime.platform": "פלטפורמה",
|
||||
"settings.info.runtime.os": "מערכת הפעלה",
|
||||
"settings.info.server.url": "כתובת השרת",
|
||||
"settings.info.server.root": "שורש סביבת העבודה",
|
||||
"settings.info.updates.title": "עדכונים",
|
||||
"settings.info.updates.subtitle": "בדוק אם יש גרסאות חדשות של CodeNomad.",
|
||||
"settings.info.updates.latest": "הגרסה האחרונה",
|
||||
"settings.info.updates.download": "הורד עדכון",
|
||||
"settings.info.updates.refresh": "רענן",
|
||||
"settings.info.diagnostics.copyFailed": "הגישה ללוח נדחתה. השתמש בכפתור ההורדה במקום.",
|
||||
"settings.info.diagnostics.title": "אבחון",
|
||||
"settings.info.diagnostics.subtitle": "אסוף מידע על המערכת והגרסה לדיווחי באגים.",
|
||||
"settings.info.diagnostics.copy": "Copy to clipboard",
|
||||
"settings.info.diagnostics.download": "Download .txt",
|
||||
"settings.info.diagnostics.copied": "Diagnostic info copied to clipboard.",
|
||||
"settings.info.diagnostics.copy": "העתק ללוח",
|
||||
"settings.info.diagnostics.download": "הורד .txt",
|
||||
"settings.info.diagnostics.copied": "מידע האבחון הועתק ללוח.",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ export const toolCallMessages = {
|
|||
|
||||
"toolCall.header.copyTitle": "העתק כותרת קריאת כלי",
|
||||
"toolCall.header.copyAriaLabel": "העתק כותרת קריאת כלי",
|
||||
"toolCall.header.copyOutputTitle": "העתק פלט כלי",
|
||||
"toolCall.header.copyOutputAriaLabel": "העתק פלט כלי",
|
||||
|
||||
"toolCall.header.showInputTitle": "הצג ארגומנטי כלי",
|
||||
"toolCall.header.showInputAriaLabel": "הצג ארגומנטי כלי",
|
||||
|
|
@ -12,6 +14,10 @@ export const toolCallMessages = {
|
|||
|
||||
"toolCall.io.input": "קלט כלי",
|
||||
"toolCall.io.output": "פלט כלי",
|
||||
"toolCall.io.copyInputTitle": "העתק קלט כלי",
|
||||
"toolCall.io.copyInputAriaLabel": "העתק קלט כלי",
|
||||
"toolCall.io.copyOutputTitle": "העתק פלט כלי",
|
||||
"toolCall.io.copyOutputAriaLabel": "העתק פלט כלי",
|
||||
|
||||
"toolCall.diff.label": "Diff",
|
||||
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||
|
|
@ -53,7 +59,7 @@ export const toolCallMessages = {
|
|||
"toolCall.renderer.read.detail.offset": "היסט: {offset}",
|
||||
"toolCall.renderer.read.detail.limit": "מגבלה: {limit}",
|
||||
|
||||
"toolCall.renderer.todo.empty": "אין פריטי תוכנית עדיין.",
|
||||
"toolCall.renderer.todo.empty": "אין פריטים מתוכננים",
|
||||
"toolCall.renderer.todo.status.pending": "ממתין",
|
||||
"toolCall.renderer.todo.status.inProgress": "בביצוע",
|
||||
"toolCall.renderer.todo.status.completed": "הושלם",
|
||||
|
|
@ -71,13 +77,9 @@ export const toolCallMessages = {
|
|||
"toolCall.permission.actions.allowOnce": "אפשר פעם אחת",
|
||||
"toolCall.permission.actions.alwaysAllow": "אפשר תמיד",
|
||||
"toolCall.permission.actions.deny": "דחה",
|
||||
"toolCall.permission.actions.confirmDeny": "אשר דחייה",
|
||||
"toolCall.permission.actions.cancel": "בטל",
|
||||
"toolCall.permission.shortcuts.allowOnce": "אפשר פעם אחת",
|
||||
"toolCall.permission.shortcuts.alwaysAllow": "אפשר תמיד",
|
||||
"toolCall.permission.rejectReason.label": "למה לדחות את האישור הזה?",
|
||||
"toolCall.permission.rejectReason.placeholder": "ספר ל-CodeNomad מה לעשות אחרת...",
|
||||
"toolCall.permission.rejectReason.hint": "הסוכן יראה את המשוב הזה ויוכל להתאים את הפעולה הבאה שלו.",
|
||||
"toolCall.permission.rejectReason.placeholder": "משוב אופציונלי בעת דחייה, למשל השתמש בפקודה בטוחה יותר...",
|
||||
"toolCall.permission.errors.unableToUpdate": "לא ניתן לעדכן אישור",
|
||||
|
||||
"permissionApproval.title": "בקשות",
|
||||
|
|
@ -94,11 +96,7 @@ export const toolCallMessages = {
|
|||
"permissionApproval.actions.allowOnce": "אפשר פעם אחת",
|
||||
"permissionApproval.actions.alwaysAllow": "אפשר תמיד",
|
||||
"permissionApproval.actions.deny": "דחה",
|
||||
"permissionApproval.actions.confirmDeny": "אשר דחייה",
|
||||
"permissionApproval.actions.cancel": "בטל",
|
||||
"permissionApproval.rejectReason.label": "למה לדחות את האישור הזה?",
|
||||
"permissionApproval.rejectReason.placeholder": "ספר ל-CodeNomad מה לעשות אחרת...",
|
||||
"permissionApproval.rejectReason.hint": "הסוכן יראה את המשוב הזה ויוכל להתאים את הפעולה הבאה שלו.",
|
||||
"permissionApproval.rejectReason.placeholder": "משוב אופציונלי בעת דחייה, למשל השתמש בפקודה בטוחה יותר...",
|
||||
"permissionApproval.fallbackHint": "טען סשן לקבלת מידע נוסף.",
|
||||
"permissionApproval.errors.unableToUpdatePermission": "לא ניתן לעדכן אישור",
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const appMessages = {
|
|||
"app.stopInstance.title": "インスタンスを停止",
|
||||
"app.stopInstance.confirmLabel": "停止",
|
||||
"app.stopInstance.cancelLabel": "実行を続ける",
|
||||
"app.stopInstance.toast.error": "ワークスペースの停止に失敗しました。",
|
||||
|
||||
"emptyState.logoAlt": "CodeNomad ロゴ",
|
||||
"emptyState.brandTitle": "CodeNomad",
|
||||
|
|
@ -33,4 +34,10 @@ export const appMessages = {
|
|||
"releases.devUpdateAvailable.title": "開発版が利用可能",
|
||||
"releases.devUpdateAvailable.message": "新しい開発版が利用可能です: {version}。",
|
||||
"releases.devUpdateAvailable.action": "リリースを見る",
|
||||
|
||||
"theme.mode.system": "システム",
|
||||
"theme.mode.light": "ライト",
|
||||
"theme.mode.dark": "ダーク",
|
||||
"theme.toggle.title": "テーマ: {mode}",
|
||||
"theme.toggle.ariaLabel": "テーマ: {mode}",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ export const commandMessages = {
|
|||
"unifiedPicker.sections.commands": "コマンド",
|
||||
"unifiedPicker.sections.agents": "エージェント",
|
||||
"unifiedPicker.sections.files": "ファイル",
|
||||
"unifiedPicker.sections.workspaceRoot": "ワークスペースルート",
|
||||
"unifiedPicker.badge.subagent": "サブエージェント",
|
||||
"unifiedPicker.footer.navigate": "移動",
|
||||
"unifiedPicker.footer.select": "選択",
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.certificateInstall.confirmLabel": "続行",
|
||||
"folderSelection.servers.certificateInstall.cancelLabel": "キャンセル",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "自己署名 HTTPS のリモートウィンドウを開くには、CodeNomad のローカル証明書を信頼する必要があります。",
|
||||
"folderSelection.sidecars.button": "Open SideCar",
|
||||
"folderSelection.sidecars.button": "SideCar を開く",
|
||||
|
||||
"projectRenameDialog.title": "ワークスペース名を変更",
|
||||
"projectRenameDialog.description.withLabel": "\"{label}\" のワークスペース名を更新します。",
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ export const instanceMessages = {
|
|||
"instanceShell.rightPanel.sections.plugins": "プラグイン",
|
||||
"instanceShell.rightPanel.sections.plugins.tooltip": "UI とサーバーの動作をカスタマイズし、MCP や LSP 以外の機能も追加できるプラグイン。",
|
||||
|
||||
"instanceShell.gitChanges.noSessionSelected": "Git の変更を表示するにはセッションを選択してください。",
|
||||
"instanceShell.gitChanges.loading": "Git の変更を読み込み中...",
|
||||
"instanceShell.gitChanges.empty": "Git の変更はまだありません。",
|
||||
"instanceShell.gitChanges.deleted": "削除済み",
|
||||
|
|
@ -141,6 +142,8 @@ export const instanceMessages = {
|
|||
"instanceShell.filesShell.viewerPlaceholder": "詳細な変更表示は次のステップで追加します。",
|
||||
"instanceShell.filesShell.viewerEmpty": "ファイルが選択されていません。",
|
||||
"instanceShell.filesShell.listEmpty": "このフォルダーにファイルはありません。",
|
||||
"instanceShell.filesShell.hideFiles": "ファイルを非表示",
|
||||
"instanceShell.filesShell.showFiles": "ファイルを表示",
|
||||
"instanceShell.filesShell.search.placeholder": "このフォルダーのファイルを絞り込む",
|
||||
"instanceShell.filesShell.search.ariaLabel": "このフォルダーのファイルを絞り込む",
|
||||
"instanceShell.filesShell.search.empty": "一致するファイルがありません。",
|
||||
|
|
@ -151,6 +154,13 @@ export const instanceMessages = {
|
|||
"instanceShell.filesShell.showSource": "ソースを表示",
|
||||
"instanceShell.filesShell.enableWordWrap": "折り返しを有効化",
|
||||
"instanceShell.filesShell.disableWordWrap": "折り返しを無効化",
|
||||
"instanceShell.diff.hideUnchanged": "未変更部分を非表示",
|
||||
"instanceShell.diff.showFull": "ファイル全体を表示",
|
||||
"instanceShell.diff.switchToSplit": "分割表示に切り替え",
|
||||
"instanceShell.diff.switchToUnified": "ユニファイド表示に切り替え",
|
||||
"instanceShell.diff.enableWordWrap": "折り返しを有効化",
|
||||
"instanceShell.diff.disableWordWrap": "折り返しを無効化",
|
||||
"instanceShell.worktree.create": "+ worktree を作成",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "計画を表示するにはセッションを選択してください。",
|
||||
"instanceShell.plan.empty": "まだ計画はありません。",
|
||||
|
|
@ -212,6 +222,6 @@ export const instanceMessages = {
|
|||
"opencodeBinarySelector.dialog.description": "CLI サーバーから公開されているファイルを参照します。",
|
||||
"opencodeBinarySelector.validation.invalidBinary": "OpenCode バイナリが無効です",
|
||||
"opencodeBinarySelector.validation.alreadyValidating": "すでに検証中です",
|
||||
"opencodeBinarySelector.display.systemPath": "{name} (system PATH)",
|
||||
"opencodeBinarySelector.display.systemPath": "{name} (システム PATH)",
|
||||
"opencodeBinarySelector.versionLabel": "v{version}",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const messagingMessages = {
|
|||
"messageSection.loadError.reload": "メッセージを再読み込み",
|
||||
"messageSection.scroll.toFirstAriaLabel": "最初のメッセージへスクロール",
|
||||
"messageSection.scroll.toLatestAriaLabel": "最新のメッセージへスクロール",
|
||||
"messageSection.scroll.showControlsAriaLabel": "スクロール操作を表示",
|
||||
"messageSection.scroll.enableHoldAriaLabel": "長いアシスタント返信の保持を有効にする",
|
||||
"messageSection.scroll.disableHoldAriaLabel": "長いアシスタント返信の保持を無効にする",
|
||||
"messageSection.quote.addAsQuote": "引用として追加",
|
||||
|
|
@ -80,6 +81,15 @@ export const messagingMessages = {
|
|||
"messageBlock.reasoning.indicator.hide": "非表示",
|
||||
"messageBlock.reasoning.indicator.view": "表示",
|
||||
"messageBlock.reasoning.detailsAriaLabel": "推論の詳細",
|
||||
"messageBlock.reasoning.thoughtsFallback": "思考",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.one": "{count} 秒間思考",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.other": "{count} 秒間思考",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.one": "{count} 分間思考",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.other": "{count} 分間思考",
|
||||
"messageBlock.reasoning.thoughtFor.hours.one": "{count} 時間思考",
|
||||
"messageBlock.reasoning.thoughtFor.hours.other": "{count} 時間思考",
|
||||
"messageBlock.reasoning.copyTitle": "思考をコピー",
|
||||
"messageBlock.reasoning.copyAriaLabel": "思考をコピー",
|
||||
|
||||
"codeBlockInline.actions.copy": "コピー",
|
||||
"codeBlockInline.actions.copied": "コピーしました!",
|
||||
|
|
@ -108,6 +118,14 @@ export const messagingMessages = {
|
|||
"messageItem.actions.deleteMessageFailedMessage": "メッセージの削除に失敗しました",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "削除するメッセージを選択",
|
||||
"messageItem.selection.selectForDeletion": "削除するメッセージを選択",
|
||||
"messageItem.selection.deselectForDeletion": "削除対象のメッセージ選択を解除",
|
||||
"messageItem.duration.workedFor.seconds.one": "{count}秒間処理しました",
|
||||
"messageItem.duration.workedFor.seconds.other": "{count}秒間処理しました",
|
||||
"messageItem.duration.workedFor.minutes.one": "{count}分間処理しました",
|
||||
"messageItem.duration.workedFor.minutes.other": "{count}分間処理しました",
|
||||
"messageItem.duration.workedFor.hours.one": "{count}時間処理しました",
|
||||
"messageItem.duration.workedFor.hours.other": "{count}時間処理しました",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "選択した項目({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "選択した項目を削除",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const settingsMessages = {
|
|||
"modelSelector.favoritesOnly.showAll": "すべてのモデルを表示",
|
||||
"modelSelector.favorite.add": "お気に入りに追加",
|
||||
"modelSelector.favorite.remove": "お気に入りから削除",
|
||||
"modelSelector.manageProviders": "Manage Providers",
|
||||
"modelSelector.manageProviders": "プロバイダーを管理",
|
||||
|
||||
"thinkingSelector.variant.default": "デフォルト",
|
||||
"thinkingSelector.label": "思考: {variant}",
|
||||
|
|
@ -64,66 +64,66 @@ export const settingsMessages = {
|
|||
"contextUsagePanel.labels.available": "残り",
|
||||
"contextUsagePanel.unavailable": "--",
|
||||
|
||||
"settings.title": "Settings",
|
||||
"settings.navigationAriaLabel": "Settings sections",
|
||||
"settings.close": "Close settings",
|
||||
"settings.content.eyebrow": "Workspace preferences",
|
||||
"settings.open.title": "Open settings",
|
||||
"settings.open.ariaLabel": "Open settings",
|
||||
"settings.nav.appearance": "Appearance",
|
||||
"settings.nav.notifications": "Notifications",
|
||||
"settings.nav.remote": "Remote Access",
|
||||
"settings.nav.speech": "Speech",
|
||||
"settings.nav.providers": "Providers",
|
||||
"settings.title": "設定",
|
||||
"settings.navigationAriaLabel": "設定セクション",
|
||||
"settings.close": "設定を閉じる",
|
||||
"settings.content.eyebrow": "ワークスペース設定",
|
||||
"settings.open.title": "設定を開く",
|
||||
"settings.open.ariaLabel": "設定を開く",
|
||||
"settings.nav.appearance": "外観",
|
||||
"settings.nav.notifications": "通知",
|
||||
"settings.nav.remote": "リモートアクセス",
|
||||
"settings.nav.speech": "音声",
|
||||
"settings.nav.providers": "プロバイダー",
|
||||
"settings.nav.opencode": "OpenCode",
|
||||
"settings.nav.configFiles": "Config Files",
|
||||
"settings.nav.info": "Info",
|
||||
"settings.scope.device": "This device",
|
||||
"settings.scope.server": "Server setting",
|
||||
"settings.common.enabled": "Enabled",
|
||||
"settings.nav.configFiles": "設定ファイル",
|
||||
"settings.nav.info": "情報",
|
||||
"settings.scope.device": "このデバイス",
|
||||
"settings.scope.server": "サーバー設定",
|
||||
"settings.common.enabled": "有効",
|
||||
"settings.common.disabled": "無効",
|
||||
"settings.section.appearance.title": "Appearance",
|
||||
"settings.section.appearance.subtitle": "Adjust how the app looks on this device.",
|
||||
"settings.appearance.theme.title": "Theme",
|
||||
"settings.appearance.theme.subtitle": "Choose the color mode used throughout the app.",
|
||||
"settings.appearance.theme.option.system": "Match your operating system setting",
|
||||
"settings.appearance.theme.option.light": "Use the light appearance",
|
||||
"settings.appearance.theme.option.dark": "Use the dark appearance",
|
||||
"settings.section.notifications.title": "Notifications",
|
||||
"settings.section.notifications.subtitle": "Control OS-level notifications for session activity.",
|
||||
"settings.notifications.permission.granted": "Granted",
|
||||
"settings.notifications.permission.denied": "Denied",
|
||||
"settings.notifications.permission.default": "Not granted",
|
||||
"settings.notifications.permission.unsupported": "Unsupported",
|
||||
"settings.notifications.messages.unsupportedEnvironment": "OS notifications are not supported in this environment.",
|
||||
"settings.notifications.messages.permissionDenied": "Notification permission denied. Enable notifications in your system or browser settings.",
|
||||
"settings.notifications.messages.permissionNotGranted": "Notification permission not granted.",
|
||||
"settings.notifications.messages.unsupportedGeneral": "Notifications are not supported in this environment.",
|
||||
"settings.notifications.messages.permissionGranted": "Permission granted. You can now enable notifications.",
|
||||
"settings.notifications.messages.permissionRequestDenied": "Permission denied. You may need to enable notifications in your system or browser settings.",
|
||||
"settings.notifications.sessionStatus.title": "Session status notifications",
|
||||
"settings.notifications.sessionStatus.subtitle": "Receive alerts when sessions need your attention.",
|
||||
"settings.notifications.enable.title": "Enable notifications",
|
||||
"settings.notifications.enable.permission": "Permission: {permission}",
|
||||
"settings.notifications.requestPermission.title": "Request permission",
|
||||
"settings.notifications.requestPermission.subtitle": "Allow the app to send notifications on this device.",
|
||||
"settings.notifications.requestPermission.action": "Request",
|
||||
"settings.notifications.allowVisible.title": "Notify when the app is focused",
|
||||
"settings.notifications.allowVisible.subtitle": "Keep alerts enabled even while this window is visible.",
|
||||
"settings.notifications.unsupportedNote": "Notifications are not supported in this environment. The notifications control stays disabled.",
|
||||
"settings.notifications.events.title": "Notify me when",
|
||||
"settings.notifications.events.subtitle": "Choose which session events should send alerts.",
|
||||
"settings.notifications.events.needsInput": "Session needs input",
|
||||
"settings.notifications.events.idle": "Session becomes idle",
|
||||
"settings.notifications.status.enabled": "Notifications enabled",
|
||||
"settings.notifications.status.disabled": "Notifications disabled",
|
||||
"settings.notifications.status.unsupported": "Notifications unsupported",
|
||||
"settings.section.remote.title": "Remote Access",
|
||||
"settings.section.remote.subtitle": "Review how this server is exposed on your network and secure access credentials.",
|
||||
"settings.section.appearance.title": "外観",
|
||||
"settings.section.appearance.subtitle": "このデバイスでのアプリの見た目を調整します。",
|
||||
"settings.appearance.theme.title": "テーマ",
|
||||
"settings.appearance.theme.subtitle": "アプリ全体で使うカラーモードを選択します。",
|
||||
"settings.appearance.theme.option.system": "OS の設定に合わせる",
|
||||
"settings.appearance.theme.option.light": "ライト表示を使用",
|
||||
"settings.appearance.theme.option.dark": "ダーク表示を使用",
|
||||
"settings.section.notifications.title": "通知",
|
||||
"settings.section.notifications.subtitle": "セッションの動作に関する OS レベルの通知を制御します。",
|
||||
"settings.notifications.permission.granted": "許可済み",
|
||||
"settings.notifications.permission.denied": "拒否済み",
|
||||
"settings.notifications.permission.default": "未許可",
|
||||
"settings.notifications.permission.unsupported": "未対応",
|
||||
"settings.notifications.messages.unsupportedEnvironment": "この環境では OS 通知はサポートされていません。",
|
||||
"settings.notifications.messages.permissionDenied": "通知権限が拒否されています。システムまたはブラウザー設定で通知を有効にしてください。",
|
||||
"settings.notifications.messages.permissionNotGranted": "通知権限が許可されていません。",
|
||||
"settings.notifications.messages.unsupportedGeneral": "この環境では通知はサポートされていません。",
|
||||
"settings.notifications.messages.permissionGranted": "権限が許可されました。通知を有効にできます。",
|
||||
"settings.notifications.messages.permissionRequestDenied": "権限が拒否されました。システムまたはブラウザー設定で通知を有効にする必要がある場合があります。",
|
||||
"settings.notifications.sessionStatus.title": "セッション状態の通知",
|
||||
"settings.notifications.sessionStatus.subtitle": "セッションに対応が必要なときに通知を受け取ります。",
|
||||
"settings.notifications.enable.title": "通知を有効化",
|
||||
"settings.notifications.enable.permission": "権限: {permission}",
|
||||
"settings.notifications.requestPermission.title": "権限をリクエスト",
|
||||
"settings.notifications.requestPermission.subtitle": "このデバイスでアプリから通知を送信できるようにします。",
|
||||
"settings.notifications.requestPermission.action": "リクエスト",
|
||||
"settings.notifications.allowVisible.title": "アプリにフォーカスがあるときも通知",
|
||||
"settings.notifications.allowVisible.subtitle": "このウィンドウが表示中でも通知を有効のままにします。",
|
||||
"settings.notifications.unsupportedNote": "この環境では通知はサポートされていません。通知コントロールは無効のままです。",
|
||||
"settings.notifications.events.title": "通知するタイミング",
|
||||
"settings.notifications.events.subtitle": "通知を送信するセッションイベントを選択します。",
|
||||
"settings.notifications.events.needsInput": "セッションに入力が必要",
|
||||
"settings.notifications.events.idle": "セッションが待機状態になる",
|
||||
"settings.notifications.status.enabled": "通知は有効です",
|
||||
"settings.notifications.status.disabled": "通知は無効です",
|
||||
"settings.notifications.status.unsupported": "通知は未対応です",
|
||||
"settings.section.remote.title": "リモートアクセス",
|
||||
"settings.section.remote.subtitle": "このサーバーがネットワーク上でどのように公開されているかと、アクセス認証情報を確認します。",
|
||||
"settings.section.opencode.title": "OpenCode",
|
||||
"settings.section.opencode.subtitle": "Choose the OpenCode binary and environment used for new instances.",
|
||||
"settings.opencode.runtime.title": "Runtime",
|
||||
"settings.opencode.runtime.subtitle": "Configure which OpenCode binary new instances launch with.",
|
||||
"settings.section.opencode.subtitle": "新しいインスタンスで使う OpenCode バイナリと環境を選択します。",
|
||||
"settings.opencode.runtime.title": "ランタイム",
|
||||
"settings.opencode.runtime.subtitle": "新しいインスタンスの起動に使う OpenCode バイナリを設定します。",
|
||||
"settings.opencode.logLevel.title": "OpenCode のログレベル",
|
||||
"settings.opencode.logLevel.subtitle": "新しい OpenCode インスタンスの起動時に使うログレベルを設定します。",
|
||||
"settings.opencode.logLevel.selector.title": "ログ出力の詳細度",
|
||||
|
|
@ -132,83 +132,100 @@ export const settingsMessages = {
|
|||
"settings.opencode.logLevel.option.info": "情報",
|
||||
"settings.opencode.logLevel.option.warn": "警告",
|
||||
"settings.opencode.logLevel.option.error": "エラー",
|
||||
"settings.configFiles.title": "Config Files",
|
||||
"settings.configFiles.subtitle": "Edit allowlisted global configuration files on this server.",
|
||||
"settings.configFiles.selector.label": "File",
|
||||
"settings.configFiles.selector.placeholder": "Select config file",
|
||||
"settings.configFiles.actions.save": "Save",
|
||||
"settings.configFiles.actions.reload": "Reload",
|
||||
"settings.configFiles.state.unsaved": "Unsaved changes",
|
||||
"settings.configFiles.state.notCreated": "File will be created on save",
|
||||
"settings.configFiles.empty": "No config file selected.",
|
||||
"settings.configFiles.errors.loadList": "Failed to load config files.",
|
||||
"settings.configFiles.errors.loadContent": "Failed to load config file.",
|
||||
"settings.configFiles.errors.save": "Failed to save config file.",
|
||||
"settings.configFiles.toast.saveSuccess": "Config file saved.",
|
||||
"settings.configFiles.toast.saveError": "Failed to save config file.",
|
||||
"settings.configFiles.confirmDiscard.message": "Discard unsaved changes before continuing?",
|
||||
"settings.configFiles.confirmDiscard.confirmLabel": "Discard changes",
|
||||
"settings.configFiles.confirmDiscard.cancelLabel": "Keep editing",
|
||||
"settings.configFiles.title": "設定ファイル",
|
||||
"settings.configFiles.subtitle": "このサーバーで許可リストに登録されたグローバル設定ファイルを編集します。",
|
||||
"settings.configFiles.selector.label": "ファイル",
|
||||
"settings.configFiles.selector.placeholder": "設定ファイルを選択",
|
||||
"settings.configFiles.actions.save": "保存",
|
||||
"settings.configFiles.actions.reload": "再読み込み",
|
||||
"settings.configFiles.state.unsaved": "未保存の変更",
|
||||
"settings.configFiles.state.notCreated": "保存時にファイルが作成されます",
|
||||
"settings.configFiles.empty": "設定ファイルが選択されていません。",
|
||||
"settings.configFiles.errors.loadList": "設定ファイルの読み込みに失敗しました。",
|
||||
"settings.configFiles.errors.loadContent": "設定ファイルの内容を読み込めませんでした。",
|
||||
"settings.configFiles.errors.save": "設定ファイルの保存に失敗しました。",
|
||||
"settings.configFiles.toast.saveSuccess": "設定ファイルを保存しました。",
|
||||
"settings.configFiles.toast.saveError": "設定ファイルの保存に失敗しました。",
|
||||
"settings.configFiles.confirmDiscard.message": "続行する前に未保存の変更を破棄しますか?",
|
||||
"settings.configFiles.confirmDiscard.confirmLabel": "変更を破棄",
|
||||
"settings.configFiles.confirmDiscard.cancelLabel": "編集を続ける",
|
||||
|
||||
"settings.providers.title": "Provider Authentication",
|
||||
"settings.providers.subtitle": "Connect API keys or OAuth accounts for the active OpenCode instance.",
|
||||
"settings.providers.refresh": "Refresh",
|
||||
"settings.providers.selectProvider": "Select provider",
|
||||
"settings.providers.available.title": "Available Providers",
|
||||
"settings.providers.configured.title": "Configured Providers",
|
||||
"settings.providers.loading": "Loading provider authentication methods...",
|
||||
"settings.providers.empty.noInstance": "Start an OpenCode workspace before managing provider authentication.",
|
||||
"settings.providers.empty.noProviders": "No providers are available from this OpenCode instance.",
|
||||
"settings.providers.empty.noConfiguredProviders": "No providers are currently configured for this OpenCode instance.",
|
||||
"settings.providers.models.one": "{count} model",
|
||||
"settings.providers.models.other": "{count} models",
|
||||
"settings.providers.actions.connect": "Connect",
|
||||
"settings.providers.actions.disconnect": "Disconnect",
|
||||
"settings.providers.actions.remove": "Remove saved auth",
|
||||
"settings.providers.actions.close": "Close",
|
||||
"settings.providers.actions.continue": "Continue",
|
||||
"settings.providers.actions.working": "Working...",
|
||||
"settings.providers.method.title": "Authentication method",
|
||||
"settings.providers.method.subtitle": "Choose the flow OpenCode exposes for this provider.",
|
||||
"settings.providers.method.api": "API key",
|
||||
"settings.providers.title": "プロバイダー認証",
|
||||
"settings.providers.subtitle": "アクティブな OpenCode インスタンスで使う API キーまたは OAuth アカウントを接続します。",
|
||||
"settings.providers.refresh": "更新",
|
||||
"settings.providers.selectProvider": "プロバイダーを選択",
|
||||
"settings.providers.available.title": "利用可能なプロバイダー",
|
||||
"settings.providers.configured.title": "設定済みプロバイダー",
|
||||
"settings.providers.loading": "プロバイダー認証方法を読み込み中...",
|
||||
"settings.providers.empty.noInstance": "プロバイダー認証を管理する前に OpenCode ワークスペースを開始してください。",
|
||||
"settings.providers.empty.noProviders": "この OpenCode インスタンスで利用可能なプロバイダーはありません。",
|
||||
"settings.providers.empty.noConfiguredProviders": "この OpenCode インスタンスには現在設定済みのプロバイダーがありません。",
|
||||
"settings.providers.models.one": "{count} 個のモデル",
|
||||
"settings.providers.models.other": "{count} 個のモデル",
|
||||
"settings.providers.actions.connect": "接続",
|
||||
"settings.providers.actions.disconnect": "切断",
|
||||
"settings.providers.actions.remove": "保存済み認証を削除",
|
||||
"settings.providers.actions.close": "閉じる",
|
||||
"settings.providers.actions.continue": "続行",
|
||||
"settings.providers.actions.working": "処理中...",
|
||||
"settings.providers.method.title": "認証方法",
|
||||
"settings.providers.method.subtitle": "このプロバイダーで OpenCode が提供するフローを選択します。",
|
||||
"settings.providers.method.api": "API キー",
|
||||
"settings.providers.method.oauth": "OAuth",
|
||||
"settings.providers.method.mixed": "API key and OAuth",
|
||||
"settings.providers.method.fallback": "API key",
|
||||
"settings.providers.status.available": "Available",
|
||||
"settings.providers.auth.title": "Connect {provider}",
|
||||
"settings.providers.auth.subtitle": "Authentication is saved inside the active OpenCode server.",
|
||||
"settings.providers.apiKey.label": "API key",
|
||||
"settings.providers.apiKey.placeholder": "Paste provider API key",
|
||||
"settings.providers.oauth.noPrompts": "This OAuth method does not need additional details before opening the browser.",
|
||||
"settings.providers.oauth.promptsUnsupported": "This OAuth method requires provider-specific prompt inputs that are not supported by the current SDK build.",
|
||||
"settings.providers.oauth.enterCode": "Complete the browser flow, then paste the authorization code.",
|
||||
"settings.providers.oauth.openPage": "Open authorization page",
|
||||
"settings.providers.oauth.openHere": "Open in this tab",
|
||||
"settings.providers.oauth.copyLink": "Copy link",
|
||||
"settings.providers.oauth.linkCopied": "Link copied",
|
||||
"settings.providers.oauth.popupBlocked": "Your browser may have blocked the authorization page. Use the actions above to continue the sign-in flow.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "OpenCode authorization",
|
||||
"settings.providers.oauth.popup.loadingBody": "Preparing authorization page...",
|
||||
"settings.providers.oauth.codeLabel": "Authorization code",
|
||||
"settings.providers.oauth.codePlaceholder": "Paste code from provider",
|
||||
"settings.providers.oauth.submitCode": "Submit code",
|
||||
"settings.providers.oauth.waitingTitle": "Waiting for authorization",
|
||||
"settings.providers.oauth.cancelWait": "Cancel",
|
||||
"settings.providers.prompt.selectPlaceholder": "Select an option",
|
||||
"settings.providers.success": "Provider authentication saved. Provider data has been refreshed.",
|
||||
"settings.providers.source.api": "Saved provider authentication",
|
||||
"settings.providers.source.config": "Configured in opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configured by custom provider logic",
|
||||
"settings.providers.source.env": "Configured from environment variables",
|
||||
"settings.providers.source.unknown": "Configured provider source",
|
||||
"settings.providers.errors.loadFailed": "Failed to load provider authentication methods.",
|
||||
"settings.providers.errors.noAuthorization": "Provider did not return authorization data.",
|
||||
"settings.providers.errors.authorizationFailed": "Authorization failed.",
|
||||
"settings.providers.errors.removeFailed": "Failed to remove provider authentication.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "This provider is configured from environment variables and cannot be disconnected from the UI.",
|
||||
"settings.providers.method.mixed": "API キーと OAuth",
|
||||
"settings.providers.method.fallback": "API キー",
|
||||
"settings.providers.status.available": "利用可能",
|
||||
"settings.providers.auth.title": "{provider} に接続",
|
||||
"settings.providers.auth.subtitle": "認証情報はアクティブな OpenCode サーバー内に保存されます。",
|
||||
"settings.providers.apiKey.label": "API キー",
|
||||
"settings.providers.apiKey.placeholder": "プロバイダーの API キーを貼り付け",
|
||||
"settings.providers.oauth.noPrompts": "この OAuth 方法では、ブラウザーを開く前に追加情報は不要です。",
|
||||
"settings.providers.oauth.promptsUnsupported": "この OAuth 方法にはプロバイダー固有の入力が必要ですが、現在の SDK ビルドではサポートされていません。",
|
||||
"settings.providers.oauth.enterCode": "ブラウザーでのフローを完了してから、認可コードを貼り付けてください。",
|
||||
"settings.providers.oauth.openPage": "認可ページを開く",
|
||||
"settings.providers.oauth.openHere": "このタブで開く",
|
||||
"settings.providers.oauth.copyLink": "リンクをコピー",
|
||||
"settings.providers.oauth.linkCopied": "リンクをコピーしました",
|
||||
"settings.providers.oauth.popupBlocked": "ブラウザーが認可ページをブロックした可能性があります。上の操作でサインインフローを続行してください。",
|
||||
"settings.providers.oauth.popup.loadingTitle": "OpenCode 認可",
|
||||
"settings.providers.oauth.popup.loadingBody": "認可ページを準備中...",
|
||||
"settings.providers.oauth.codeLabel": "認可コード",
|
||||
"settings.providers.oauth.codePlaceholder": "プロバイダーからのコードを貼り付け",
|
||||
"settings.providers.oauth.submitCode": "コードを送信",
|
||||
"settings.providers.oauth.waitingTitle": "認可を待機中",
|
||||
"settings.providers.oauth.cancelWait": "キャンセル",
|
||||
"settings.providers.prompt.selectPlaceholder": "オプションを選択",
|
||||
"settings.providers.success": "プロバイダー認証を保存しました。プロバイダーデータを更新しました。",
|
||||
"settings.providers.source.api": "保存済みプロバイダー認証",
|
||||
"settings.providers.source.config": "opencode.jsonc で設定済み",
|
||||
"settings.providers.source.custom": "カスタムプロバイダーロジックで設定済み",
|
||||
"settings.providers.source.env": "環境変数から設定済み",
|
||||
"settings.providers.source.unknown": "設定済みプロバイダーソース",
|
||||
"settings.providers.errors.loadFailed": "プロバイダー認証方法の読み込みに失敗しました。",
|
||||
"settings.providers.errors.noAuthorization": "プロバイダーが認可データを返しませんでした。",
|
||||
"settings.providers.errors.authorizationFailed": "認可に失敗しました。",
|
||||
"settings.providers.errors.removeFailed": "プロバイダー認証の削除に失敗しました。",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "このプロバイダーは環境変数から設定されているため、UI から切断できません。",
|
||||
|
||||
"settings.appearance.behavior.title": "操作",
|
||||
"settings.appearance.behavior.subtitle": "メッセージ、差分、入力の既定値。",
|
||||
"settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets",
|
||||
"settings.behavior.expansionPreset.minimal.title": "Minimal",
|
||||
"settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.",
|
||||
"settings.behavior.expansionPreset.balanced.title": "Balanced",
|
||||
"settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.",
|
||||
"settings.behavior.expansionPreset.detailed.title": "Detailed",
|
||||
"settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.",
|
||||
"settings.behavior.expansionPreset.everything.title": "Everything",
|
||||
"settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.",
|
||||
"settings.behavior.expansionPreset.custom.title": "Custom",
|
||||
"settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.",
|
||||
"settings.behavior.expansionDefaults.title": "Expansion defaults",
|
||||
"settings.behavior.expansionDefaults.itemColumn": "Item",
|
||||
"settings.behavior.expansionDefaults.stateColumn": "Default state",
|
||||
"settings.behavior.expansionDefaults.thinking": "thinking",
|
||||
"settings.behavior.expansionDefaults.otherTools": "Other tools",
|
||||
"settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}",
|
||||
"settings.behavior.keyboardHints.title": "キーボードショートカットのヒント",
|
||||
"settings.behavior.keyboardHints.subtitle": "UI全体でキーボードショートカットのヒントを表示します。",
|
||||
"settings.behavior.thinking.title": "思考セクション",
|
||||
|
|
@ -237,26 +254,26 @@ export const settingsMessages = {
|
|||
"settings.behavior.keepUnseenSubagentIdle.subtitle": "サブエージェントの idle マーカーを 5 秒後に隠さず、表示するまで残します。",
|
||||
"settings.behavior.tauriNativeEventTransport.title": "Tauri ネイティブイベント転送",
|
||||
"settings.behavior.tauriNativeEventTransport.subtitle": "Tauri で Rust ネイティブのデスクトップイベント転送を使います。無効にすると browser EventSource 経路に戻ります。",
|
||||
"settings.behavior.promptVoiceInput.title": "Prompt voice input",
|
||||
"settings.behavior.promptVoiceInput.subtitle": "Show the microphone control for speech-to-text prompt input when speech is configured.",
|
||||
"settings.behavior.promptVoiceInput.title": "プロンプト音声入力",
|
||||
"settings.behavior.promptVoiceInput.subtitle": "音声が設定されている場合、音声文字起こし用のマイク操作を表示します。",
|
||||
"settings.behavior.promptSubmit.title": "Enterで送信",
|
||||
"settings.behavior.promptSubmit.subtitle": "Enterで送信し、Cmd/Ctrl+Enterで改行します。",
|
||||
"settings.speech.title": "音声",
|
||||
"settings.speech.subtitle": "今すぐ音声入力を設定し、今後の機能のために音声合成の基盤も準備します。",
|
||||
"settings.speech.provider.title": "プロバイダー",
|
||||
"settings.speech.provider.subtitle": "音声リクエストはサーバー側の音声アダプターを使用します。",
|
||||
"settings.speech.provider.openaiCompatible": "OpenAI-compatible",
|
||||
"settings.speech.provider.openaiCompatible": "OpenAI 互換",
|
||||
"settings.speech.status.loading": "設定を確認しています...",
|
||||
"settings.speech.status.configured": "設定済み",
|
||||
"settings.speech.status.missing": "APIキーがありません",
|
||||
"settings.speech.status.error": "音声サービスを利用できません",
|
||||
"settings.speech.apiKey.title": "API key",
|
||||
"settings.speech.apiKey.title": "API キー",
|
||||
"settings.speech.apiKey.subtitle": "CodeNomadが管理する音声リクエストに使用されます。",
|
||||
"settings.speech.apiKey.placeholder": "新しいAPIキーを入力",
|
||||
"settings.speech.apiKey.storedNote": "保存済みのAPIキーは非表示になっています。置き換えるには新しい値を入力し、そのまま使うには空欄のままにしてください。",
|
||||
"settings.speech.apiKey.clearAction": "保存済みキーを削除",
|
||||
"settings.speech.apiKey.clearPending": "保存すると、保存済みのAPIキーは削除されます。",
|
||||
"settings.speech.baseUrl.title": "Base URL",
|
||||
"settings.speech.baseUrl.title": "ベース URL",
|
||||
"settings.speech.baseUrl.subtitle": "OpenAI互換の音声エンドポイント用の任意の上書き設定です。",
|
||||
"settings.speech.baseUrl.placeholder": "https://api.openai.com/v1",
|
||||
"settings.speech.sttModel.title": "文字起こしモデル",
|
||||
|
|
@ -267,8 +284,8 @@ export const settingsMessages = {
|
|||
"settings.speech.ttsVoice.subtitle": "将来の再生機能のために予約されている既定の音声合成ボイスです。",
|
||||
"settings.speech.playbackMode.title": "再生モード",
|
||||
"settings.speech.playbackMode.subtitle": "音声が届き次第再生を始めるか、ファイル全体の生成後に再生するかを選択します。",
|
||||
"settings.speech.playbackMode.streaming": "Streaming",
|
||||
"settings.speech.playbackMode.buffered": "Buffered",
|
||||
"settings.speech.playbackMode.streaming": "ストリーミング",
|
||||
"settings.speech.playbackMode.buffered": "バッファ再生",
|
||||
"settings.speech.ttsFormat.title": "出力形式",
|
||||
"settings.speech.ttsFormat.subtitle": "音声合成の出力形式を選択します。ストリーミング対応はプロバイダーとブラウザーに依存します。",
|
||||
"settings.speech.help": "プロンプト音声入力は音声文字起こしが設定され対応している場合に表示されます。メッセージ再生にはここで選んだTTSモードと形式が使われます。",
|
||||
|
|
@ -285,42 +302,42 @@ export const settingsMessages = {
|
|||
"settings.speech.save.saved": "保存済み",
|
||||
"settings.speech.save.unsaved": "未保存の変更",
|
||||
"settings.speech.save.error": "保存に失敗しました",
|
||||
"settings.nav.sidecars": "SideCars",
|
||||
"settings.section.sidecars.eyebrow": "Server services",
|
||||
"settings.section.sidecars.title": "SideCars",
|
||||
"settings.section.sidecars.subtitle": "Configure local services listening on fixed ports that the server can proxy into tabs.",
|
||||
"sidecars.form.name": "Name",
|
||||
"sidecars.form.validation": "Enter a valid SideCar name and port.",
|
||||
"sidecars.form.port": "Port",
|
||||
"sidecars.form.insecure": "Use HTTP",
|
||||
"sidecars.form.protocol": "Protocol",
|
||||
"sidecars.form.protocol.help": "Choose how the proxy should connect to the local service.",
|
||||
"settings.nav.sidecars": "SideCar",
|
||||
"settings.section.sidecars.eyebrow": "サーバーサービス",
|
||||
"settings.section.sidecars.title": "SideCar",
|
||||
"settings.section.sidecars.subtitle": "サーバーがタブへプロキシできる、固定ポートで待ち受けるローカルサービスを設定します。",
|
||||
"sidecars.form.name": "名前",
|
||||
"sidecars.form.validation": "有効な SideCar 名とポートを入力してください。",
|
||||
"sidecars.form.port": "ポート",
|
||||
"sidecars.form.insecure": "HTTP を使用",
|
||||
"sidecars.form.protocol": "プロトコル",
|
||||
"sidecars.form.protocol.help": "プロキシがローカルサービスへ接続する方法を選択します。",
|
||||
"sidecars.form.protocol.https": "HTTPS",
|
||||
"sidecars.form.protocol.http": "HTTP",
|
||||
"sidecars.form.prefixMode": "Prefix mode",
|
||||
"sidecars.form.prefixMode.help": "Choose whether the SideCar receives the public /sidecars path prefix.",
|
||||
"sidecars.form.prefixMode.strip": "Strip prefix",
|
||||
"sidecars.form.prefixMode.preserve": "Preserve prefix",
|
||||
"sidecars.form.add": "Add SideCar",
|
||||
"sidecars.kind.port": "Port",
|
||||
"sidecars.status.running": "Running",
|
||||
"sidecars.status.stopped": "Stopped",
|
||||
"sidecars.basePath": "Base path",
|
||||
"sidecars.settings.listTitle": "Configured SideCars",
|
||||
"sidecars.settings.listSubtitle": "Review the port-based SideCars available in the picker.",
|
||||
"sidecars.settings.empty": "No SideCars configured yet.",
|
||||
"sidecars.picker.title": "Open SideCar",
|
||||
"sidecars.picker.loading": "Loading SideCars...",
|
||||
"sidecars.picker.subtitle": "Choose an available SideCar to open in a new tab.",
|
||||
"sidecars.picker.empty": "No port-based SideCars are available yet.",
|
||||
"sidecars.picker.close": "Close",
|
||||
"sidecars.open.errorTitle": "Unable to open SideCar",
|
||||
"sidecars.open.notFound": "SideCar not found.",
|
||||
"sidecars.open.notRunning": "SideCar is not reachable on its configured port.",
|
||||
"sidecars.back": "Back",
|
||||
"sidecars.refresh": "Refresh",
|
||||
"sidecars.path": "Path",
|
||||
"sidecars.go": "Go",
|
||||
"sidecars.form.prefixMode": "プレフィックスモード",
|
||||
"sidecars.form.prefixMode.help": "SideCar が公開 /sidecars パスプレフィックスを受け取るかを選択します。",
|
||||
"sidecars.form.prefixMode.strip": "プレフィックスを削除",
|
||||
"sidecars.form.prefixMode.preserve": "プレフィックスを保持",
|
||||
"sidecars.form.add": "SideCar を追加",
|
||||
"sidecars.kind.port": "ポート",
|
||||
"sidecars.status.running": "実行中",
|
||||
"sidecars.status.stopped": "停止中",
|
||||
"sidecars.basePath": "ベースパス",
|
||||
"sidecars.settings.listTitle": "設定済み SideCar",
|
||||
"sidecars.settings.listSubtitle": "ピッカーで利用可能なポートベースの SideCar を確認します。",
|
||||
"sidecars.settings.empty": "SideCar はまだ設定されていません。",
|
||||
"sidecars.picker.title": "SideCar を開く",
|
||||
"sidecars.picker.loading": "SideCar を読み込み中...",
|
||||
"sidecars.picker.subtitle": "新しいタブで開く利用可能な SideCar を選択します。",
|
||||
"sidecars.picker.empty": "利用可能なポートベースの SideCar はまだありません。",
|
||||
"sidecars.picker.close": "閉じる",
|
||||
"sidecars.open.errorTitle": "SideCar を開けません",
|
||||
"sidecars.open.notFound": "SideCar が見つかりません。",
|
||||
"sidecars.open.notRunning": "SideCar は設定されたポートで到達できません。",
|
||||
"sidecars.back": "戻る",
|
||||
"sidecars.refresh": "更新",
|
||||
"sidecars.path": "パス",
|
||||
"sidecars.go": "移動",
|
||||
|
||||
// Toast History Panel
|
||||
"toastHistory.title": "通知履歴",
|
||||
|
|
@ -343,25 +360,25 @@ export const settingsMessages = {
|
|||
"toastHistory.deleteItem": "通知を削除",
|
||||
|
||||
// Info Section
|
||||
"settings.section.info.title": "About",
|
||||
"settings.section.info.subtitle": "View version, runtime, and gather diagnostic information.",
|
||||
"settings.info.version.server": "Server version",
|
||||
"settings.info.version.ui": "UI version",
|
||||
"settings.info.version.uiSource": "UI source",
|
||||
"settings.info.runtime.type": "Runtime",
|
||||
"settings.info.runtime.platform": "Platform",
|
||||
"settings.info.runtime.os": "Operating system",
|
||||
"settings.info.server.url": "Server URL",
|
||||
"settings.info.server.root": "Workspace root",
|
||||
"settings.info.updates.title": "Updates",
|
||||
"settings.info.updates.subtitle": "Check for new releases of CodeNomad.",
|
||||
"settings.info.updates.latest": "Latest version",
|
||||
"settings.info.updates.download": "Download update",
|
||||
"settings.info.updates.refresh": "Refresh",
|
||||
"settings.info.diagnostics.copyFailed": "Clipboard access denied. Use the download button instead.",
|
||||
"settings.info.diagnostics.title": "Diagnostics",
|
||||
"settings.section.info.title": "概要",
|
||||
"settings.section.info.subtitle": "バージョン、ランタイムを確認し、診断情報を収集します。",
|
||||
"settings.info.version.server": "サーバーバージョン",
|
||||
"settings.info.version.ui": "UI バージョン",
|
||||
"settings.info.version.uiSource": "UI ソース",
|
||||
"settings.info.runtime.type": "ランタイム",
|
||||
"settings.info.runtime.platform": "プラットフォーム",
|
||||
"settings.info.runtime.os": "オペレーティングシステム",
|
||||
"settings.info.server.url": "サーバー URL",
|
||||
"settings.info.server.root": "ワークスペースルート",
|
||||
"settings.info.updates.title": "更新",
|
||||
"settings.info.updates.subtitle": "CodeNomad の新しいリリースを確認します。",
|
||||
"settings.info.updates.latest": "最新バージョン",
|
||||
"settings.info.updates.download": "更新をダウンロード",
|
||||
"settings.info.updates.refresh": "更新",
|
||||
"settings.info.diagnostics.copyFailed": "クリップボードへのアクセスが拒否されました。代わりにダウンロードボタンを使用してください。",
|
||||
"settings.info.diagnostics.title": "診断",
|
||||
"settings.info.diagnostics.subtitle": "バグ報告用にシステム情報とバージョン情報を収集します。",
|
||||
"settings.info.diagnostics.copy": "Copy to clipboard",
|
||||
"settings.info.diagnostics.download": "Download .txt",
|
||||
"settings.info.diagnostics.copied": "Diagnostic info copied to clipboard.",
|
||||
"settings.info.diagnostics.copy": "クリップボードにコピー",
|
||||
"settings.info.diagnostics.download": ".txt をダウンロード",
|
||||
"settings.info.diagnostics.copied": "診断情報をクリップボードにコピーしました。",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -2,19 +2,25 @@ export const toolCallMessages = {
|
|||
"toolCall.pending.waitingToRun": "実行待ち...",
|
||||
"toolCall.error.label": "エラー:",
|
||||
|
||||
"toolCall.header.copyTitle": "Copy tool call title",
|
||||
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
||||
"toolCall.header.copyTitle": "ツール呼び出しのタイトルをコピー",
|
||||
"toolCall.header.copyAriaLabel": "ツール呼び出しのタイトルをコピー",
|
||||
"toolCall.header.copyOutputTitle": "ツール出力をコピー",
|
||||
"toolCall.header.copyOutputAriaLabel": "ツール出力をコピー",
|
||||
|
||||
"toolCall.header.showInputTitle": "Show Tool Arguments",
|
||||
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
|
||||
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
|
||||
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
|
||||
"toolCall.header.showInputTitle": "ツール引数を表示",
|
||||
"toolCall.header.showInputAriaLabel": "ツール引数を表示",
|
||||
"toolCall.header.hideInputTitle": "ツール引数を非表示",
|
||||
"toolCall.header.hideInputAriaLabel": "ツール引数を非表示",
|
||||
|
||||
"toolCall.io.input": "Tool Input",
|
||||
"toolCall.io.output": "Tool Output",
|
||||
"toolCall.io.input": "ツール入力",
|
||||
"toolCall.io.output": "ツール出力",
|
||||
"toolCall.io.copyInputTitle": "ツール入力をコピー",
|
||||
"toolCall.io.copyInputAriaLabel": "ツール入力をコピー",
|
||||
"toolCall.io.copyOutputTitle": "ツール出力をコピー",
|
||||
"toolCall.io.copyOutputAriaLabel": "ツール出力をコピー",
|
||||
|
||||
"toolCall.diff.label": "Diff",
|
||||
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||
"toolCall.diff.label": "差分",
|
||||
"toolCall.diff.label.withPath": "差分 · {path}",
|
||||
"toolCall.diff.viewMode.ariaLabel": "diff 表示モード",
|
||||
"toolCall.diff.viewMode.split": "分割",
|
||||
"toolCall.diff.viewMode.unified": "ユニファイド",
|
||||
|
|
@ -53,7 +59,7 @@ export const toolCallMessages = {
|
|||
"toolCall.renderer.read.detail.offset": "オフセット: {offset}",
|
||||
"toolCall.renderer.read.detail.limit": "上限: {limit}",
|
||||
|
||||
"toolCall.renderer.todo.empty": "まだ項目がありません。",
|
||||
"toolCall.renderer.todo.empty": "計画済みの項目はありません",
|
||||
"toolCall.renderer.todo.status.pending": "保留",
|
||||
"toolCall.renderer.todo.status.inProgress": "進行中",
|
||||
"toolCall.renderer.todo.status.completed": "完了",
|
||||
|
|
@ -71,13 +77,9 @@ export const toolCallMessages = {
|
|||
"toolCall.permission.actions.allowOnce": "一度だけ許可",
|
||||
"toolCall.permission.actions.alwaysAllow": "常に許可",
|
||||
"toolCall.permission.actions.deny": "拒否",
|
||||
"toolCall.permission.actions.confirmDeny": "拒否を確定",
|
||||
"toolCall.permission.actions.cancel": "キャンセル",
|
||||
"toolCall.permission.shortcuts.allowOnce": "一度だけ許可",
|
||||
"toolCall.permission.shortcuts.alwaysAllow": "常に許可",
|
||||
"toolCall.permission.rejectReason.label": "この許可を拒否する理由は?",
|
||||
"toolCall.permission.rejectReason.placeholder": "CodeNomad に別の対応を伝えてください...",
|
||||
"toolCall.permission.rejectReason.hint": "エージェントはこのフィードバックを見て、次の操作を調整できます。",
|
||||
"toolCall.permission.rejectReason.placeholder": "拒否時の任意フィードバック(例: より安全なコマンドを使う)...",
|
||||
"toolCall.permission.errors.unableToUpdate": "許可を更新できません",
|
||||
|
||||
"permissionApproval.title": "リクエスト",
|
||||
|
|
@ -94,11 +96,7 @@ export const toolCallMessages = {
|
|||
"permissionApproval.actions.allowOnce": "一度だけ許可",
|
||||
"permissionApproval.actions.alwaysAllow": "常に許可",
|
||||
"permissionApproval.actions.deny": "拒否",
|
||||
"permissionApproval.actions.confirmDeny": "拒否を確定",
|
||||
"permissionApproval.actions.cancel": "キャンセル",
|
||||
"permissionApproval.rejectReason.label": "この許可を拒否する理由は?",
|
||||
"permissionApproval.rejectReason.placeholder": "CodeNomad に別の対応を伝えてください...",
|
||||
"permissionApproval.rejectReason.hint": "エージェントはこのフィードバックを見て、次の操作を調整できます。",
|
||||
"permissionApproval.rejectReason.placeholder": "拒否時の任意フィードバック(例: より安全なコマンドを使う)...",
|
||||
"permissionApproval.fallbackHint": "詳しい情報を見るにはセッションを読み込んでください。",
|
||||
"permissionApproval.errors.unableToUpdatePermission": "許可を更新できません",
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const appMessages = {
|
|||
"app.stopInstance.title": "उदाहरण रोक्नुहोस्",
|
||||
"app.stopInstance.confirmLabel": "रोक्नुहोस्",
|
||||
"app.stopInstance.cancelLabel": "चालु राख्नुहोस्",
|
||||
"app.stopInstance.toast.error": "कार्यस्थान रोक्न असफल भयो।",
|
||||
|
||||
"emptyState.logoAlt": "CodeNomad लोगो",
|
||||
"emptyState.brandTitle": "CodeNomad",
|
||||
|
|
|
|||
|
|
@ -75,15 +75,15 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.connectSubtitle": "रिमोट CodeNomad सर्भर बचत गर्नुहोस् र यसलाई नयाँ विन्डोमा खोल्नुहोस्",
|
||||
"folderSelection.servers.connectButton": "सर्भरमा जडान गर्नुहोस्",
|
||||
"folderSelection.servers.remove": "बचत गरिएको सर्भर हटाउनुहोस्",
|
||||
"folderSelection.servers.skipTls": "Self-signed TLS",
|
||||
"folderSelection.servers.skipTls": "स्व-हस्ताक्षरित TLS",
|
||||
"folderSelection.servers.errorTitle": "रिमोट जडान असफल भयो",
|
||||
"folderSelection.servers.dialog.title": "सर्भरमा जडान गर्नुहोस्",
|
||||
"folderSelection.servers.dialog.description": "रिमोट CodeNomad सर्भर थप्नुहोस् र वैकल्पिक रूपमा यसलाई तुरुन्तै खोल्नुहोस्।",
|
||||
"folderSelection.servers.dialog.name": "सर्भरको नाम",
|
||||
"folderSelection.servers.dialog.namePlaceholder": "Production Server",
|
||||
"folderSelection.servers.dialog.namePlaceholder": "उत्पादन सर्भर",
|
||||
"folderSelection.servers.dialog.url": "सर्भर URL",
|
||||
"folderSelection.servers.dialog.urlPlaceholder": "https://server.example.com",
|
||||
"folderSelection.servers.dialog.skipTls": "Self-signed प्रमाणपत्रहरूको लागि TLS प्रमाणीकरण छोड्नुहोस्।",
|
||||
"folderSelection.servers.dialog.skipTls": "स्व-हस्ताक्षरित प्रमाणपत्रहरूको लागि TLS प्रमाणीकरण छोड्नुहोस्।",
|
||||
"folderSelection.servers.dialog.cancel": "रद्द गर्नुहोस्",
|
||||
"folderSelection.servers.dialog.save": "बचत गर्नुहोस्",
|
||||
"folderSelection.servers.dialog.connect": "जडान गर्नुहोस्",
|
||||
|
|
@ -91,10 +91,10 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.dialog.errorRequired": "सर्भरको नाम र URL आवश्यक छ।",
|
||||
"folderSelection.servers.dialog.errorConnect": "रिमोट सर्भरमा जडान गर्न सकिएन।",
|
||||
"folderSelection.servers.certificateInstall.title": "स्थानीय प्रमाणपत्र स्थापना गर्नुहोस्",
|
||||
"folderSelection.servers.certificateInstall.confirmMessage": "CodeNomad लाई self-signed HTTPS रिमोट विन्डोहरू खोल्न स्थानीय प्रमाणपत्र स्थापना गर्न आवश्यक छ। यो प्रमाणपत्र तपाईंको मेसिनमा स्थानीय डेस्कटप प्रोक्सी ट्राफिकको लागि मात्र प्रयोग गरिन्छ। तपाईंको अपरेटिङ सिस्टमले यसपछि दोस्रो प्रमाणपत्र प्रम्प्ट देखाउन सक्छ।",
|
||||
"folderSelection.servers.certificateInstall.confirmMessage": "CodeNomad लाई स्व-हस्ताक्षरित HTTPS रिमोट विन्डोहरू खोल्न स्थानीय प्रमाणपत्र स्थापना गर्न आवश्यक छ। यो प्रमाणपत्र तपाईंको मेसिनमा स्थानीय डेस्कटप प्रोक्सी ट्राफिकको लागि मात्र प्रयोग गरिन्छ। तपाईंको अपरेटिङ सिस्टमले यसपछि दोस्रो प्रमाणपत्र प्रम्प्ट देखाउन सक्छ।",
|
||||
"folderSelection.servers.certificateInstall.confirmLabel": "जारी राख्नुहोस्",
|
||||
"folderSelection.servers.certificateInstall.cancelLabel": "रद्द गर्नुहोस्",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "CodeNomad लाई self-signed HTTPS रिमोट विन्डोहरू खोल्न सक्नु अघि स्थानीय प्रमाणपत्र विश्वास गरिनु पर्छ।",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "CodeNomad लाई स्व-हस्ताक्षरित HTTPS रिमोट विन्डोहरू खोल्न सक्नु अघि स्थानीय प्रमाणपत्र विश्वास गरिनु पर्छ।",
|
||||
"folderSelection.sidecars.button": "SideCar खोल्नुहोस्",
|
||||
|
||||
"projectRenameDialog.title": "कार्यस्थान पुन: नामकरण गर्नुहोस्",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export const instanceMessages = {
|
|||
"instanceTab.status.permission": "अनुमतिको प्रतीक्षा गर्दै",
|
||||
"instanceTab.status.compacting": "संक्षिप्त गर्दै",
|
||||
"instanceTab.status.working": "काम गर्दै",
|
||||
"instanceTab.status.idle": "Idle",
|
||||
"instanceTab.status.idle": "निष्क्रिय",
|
||||
"instanceTab.status.ariaLabel": "उदाहरण स्थिति: {status}",
|
||||
"instanceTab.actions.close.ariaLabel": "उदाहरण बन्द गर्नुहोस्",
|
||||
|
||||
|
|
@ -130,10 +130,10 @@ export const instanceMessages = {
|
|||
"instanceShell.gitChanges.actions.stage": "फाइल स्टेज गर्नुहोस्",
|
||||
"instanceShell.gitChanges.actions.unstage": "फाइल अनस्टेज गर्नुहोस्",
|
||||
"instanceShell.gitChanges.commit.placeholder": "Commit सन्देश प्रविष्ट गर्नुहोस्",
|
||||
"instanceShell.gitChanges.commit.submit": "Commit",
|
||||
"instanceShell.gitChanges.commit.submitting": "Commit गर्दै...",
|
||||
"instanceShell.gitChanges.commit.success": "Commit सफलतापूर्वक सिर्जना गरियो",
|
||||
"instanceShell.gitChanges.commit.error": "Commit सिर्जना गर्न असफल भयो",
|
||||
"instanceShell.gitChanges.commit.submit": "कमिट गर्नुहोस्",
|
||||
"instanceShell.gitChanges.commit.submitting": "कमिट गर्दै...",
|
||||
"instanceShell.gitChanges.commit.success": "कमिट सफलतापूर्वक सिर्जना गरियो",
|
||||
"instanceShell.gitChanges.commit.error": "कमिट सिर्जना गर्न असफल भयो",
|
||||
|
||||
"instanceShell.filesShell.fileListTitle": "फाइल सूची",
|
||||
"instanceShell.filesShell.mobileSelectorLabel": "फाइल चयन गर्नुहोस्",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const messagingMessages = {
|
|||
"messageSection.loadError.reload": "सन्देशहरू फेरि लोड गर्नुहोस्",
|
||||
"messageSection.scroll.toFirstAriaLabel": "पहिलो सन्देशमा जानुहोस्",
|
||||
"messageSection.scroll.toLatestAriaLabel": "भर्खरको सन्देशमा जानुहोस्",
|
||||
"messageSection.scroll.showControlsAriaLabel": "स्क्रोल नियन्त्रणहरू देखाउनुहोस्",
|
||||
"messageSection.scroll.enableHoldAriaLabel": "लामो प्रतिक्रियाहरूको लागि होल्ड सक्षम गर्नुहोस्",
|
||||
"messageSection.scroll.disableHoldAriaLabel": "लामो प्रतिक्रियाहरूको लागि होल्ड अक्षम गर्नुहोस्",
|
||||
"messageSection.quote.addAsQuote": "उद्धरणको रूपमा थप्नुहोस्",
|
||||
|
|
@ -42,7 +43,7 @@ export const messagingMessages = {
|
|||
"messageTimeline.ariaLabel": "सन्देश टाइमलाइन",
|
||||
"messageTimeline.segment.user.label": "तपाईं",
|
||||
"messageTimeline.segment.assistant.label": "सहायक",
|
||||
"messageTimeline.segment.compaction.label": "Compaction",
|
||||
"messageTimeline.segment.compaction.label": "संक्षिप्तीकरण",
|
||||
"messageTimeline.tool.fallbackLabel": "उपकरण कल",
|
||||
"messageTimeline.tooltip.userFallback": "प्रयोगकर्ता सन्देश",
|
||||
"messageTimeline.tooltip.assistantFallback": "सहायक प्रतिक्रिया",
|
||||
|
|
@ -67,8 +68,8 @@ export const messagingMessages = {
|
|||
"messageBlock.usage.input": "इनपुट",
|
||||
"messageBlock.usage.output": "आउटपुट",
|
||||
"messageBlock.usage.reasoning": "तर्क (Reasoning)",
|
||||
"messageBlock.usage.cacheRead": "Cache Read",
|
||||
"messageBlock.usage.cacheWrite": "Cache Write",
|
||||
"messageBlock.usage.cacheRead": "क्यास पढाइ",
|
||||
"messageBlock.usage.cacheWrite": "क्यास लेखाइ",
|
||||
"messageBlock.usage.cost": "लागत (Cost)",
|
||||
"messageBlock.step.agentLabel": "एजेन्ट: {agent}",
|
||||
"messageBlock.step.modelLabel": "मोडेल: {model}",
|
||||
|
|
@ -78,6 +79,15 @@ export const messagingMessages = {
|
|||
"messageBlock.reasoning.indicator.hide": "लुकाउनुहोस्",
|
||||
"messageBlock.reasoning.indicator.view": "हेर्नुहोस्",
|
||||
"messageBlock.reasoning.detailsAriaLabel": "तर्क विवरणहरू",
|
||||
"messageBlock.reasoning.thoughtsFallback": "विचारहरू",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.one": "{count} सेकेन्ड सोच्यो",
|
||||
"messageBlock.reasoning.thoughtFor.seconds.other": "{count} सेकेन्ड सोच्यो",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.one": "{count} मिनेट सोच्यो",
|
||||
"messageBlock.reasoning.thoughtFor.minutes.other": "{count} मिनेट सोच्यो",
|
||||
"messageBlock.reasoning.thoughtFor.hours.one": "{count} घण्टा सोच्यो",
|
||||
"messageBlock.reasoning.thoughtFor.hours.other": "{count} घण्टा सोच्यो",
|
||||
"messageBlock.reasoning.copyTitle": "विचारहरू प्रतिलिपि गर्नुहोस्",
|
||||
"messageBlock.reasoning.copyAriaLabel": "विचारहरू प्रतिलिपि गर्नुहोस्",
|
||||
|
||||
"codeBlockInline.actions.copy": "प्रतिलिपि गर्नुहोस्",
|
||||
"codeBlockInline.actions.copied": "प्रतिलिपि गरियो!",
|
||||
|
|
@ -86,8 +96,8 @@ export const messagingMessages = {
|
|||
"messageItem.speaker.assistant": "सहायक",
|
||||
"messageItem.actions.revert": "फिर्ता",
|
||||
"messageItem.actions.revertTitle": "यहाँ सम्मका परिवर्तनहरू फिर्ता लिनुहोस् (सन्देशहरू मेटाउँछ)",
|
||||
"messageItem.actions.fork": "Fork",
|
||||
"messageItem.actions.forkTitle": "यो सन्देशबाट Fork गर्नुहोस्",
|
||||
"messageItem.actions.fork": "फोर्क",
|
||||
"messageItem.actions.forkTitle": "यो सन्देशबाट फोर्क गर्नुहोस्",
|
||||
"messageItem.actions.copy": "प्रतिलिपि गर्नुहोस्",
|
||||
"messageItem.actions.copyTitle": "सन्देश प्रतिलिपि गर्नुहोस्",
|
||||
"messageItem.actions.copied": "प्रतिलिपि गरियो!",
|
||||
|
|
@ -106,6 +116,14 @@ export const messagingMessages = {
|
|||
"messageItem.actions.deleteMessageFailedMessage": "सन्देश मेटाउन असफल भयो",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "मेटाउनको लागि सन्देश चयन गर्नुहोस्",
|
||||
"messageItem.selection.selectForDeletion": "मेटाउनको लागि सन्देश चयन गर्नुहोस्",
|
||||
"messageItem.selection.deselectForDeletion": "मेटाउनको लागि सन्देश चयन हटाउनुहोस्",
|
||||
"messageItem.duration.workedFor.seconds.one": "{count} सेकेन्ड काम गर्यो",
|
||||
"messageItem.duration.workedFor.seconds.other": "{count} सेकेन्ड काम गर्यो",
|
||||
"messageItem.duration.workedFor.minutes.one": "{count} मिनेट काम गर्यो",
|
||||
"messageItem.duration.workedFor.minutes.other": "{count} मिनेट काम गर्यो",
|
||||
"messageItem.duration.workedFor.hours.one": "{count} घण्टा काम गर्यो",
|
||||
"messageItem.duration.workedFor.hours.other": "{count} घण्टा काम गर्यो",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "चयन गरिएका वस्तुहरू ({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "चयन गरिएका वस्तुहरू मेटाउनुहोस्",
|
||||
|
|
@ -120,7 +138,7 @@ export const messagingMessages = {
|
|||
"messageSection.bulkDelete.cancelTitle": "चयन रद्द गर्नुहोस्",
|
||||
"messageSection.bulkDelete.failedTitle": "मेटाउन असफल",
|
||||
"messageSection.bulkDelete.failedMessage": "चयन गरिएका वस्तुहरू मेटाउन असफल भयो",
|
||||
"messageItem.status.queued": "Queued",
|
||||
"messageItem.status.queued": "लामबद्ध",
|
||||
"messageItem.status.generating": "सिर्जना गर्दै...",
|
||||
"messageItem.status.sending": "पठाउँदै...",
|
||||
"messageItem.status.failedToSend": "सन्देश पठाउन असफल भयो",
|
||||
|
|
@ -129,6 +147,10 @@ export const messagingMessages = {
|
|||
"messagePart.actions.deleteTitle": "यो वस्तु मेटाउनुहोस्",
|
||||
"messagePart.actions.deleteFailedTitle": "मेटाउन असफल",
|
||||
"messagePart.actions.deleteFailedMessage": "वस्तु मेटाउन असफल भयो",
|
||||
"messagePart.pastedText.summary": "टाँसिएको पाठ",
|
||||
"messagePart.pastedText.lines.one": "{count} पङ्क्ति",
|
||||
"messagePart.pastedText.lines.other": "{count} पङ्क्तिहरू",
|
||||
"messagePart.pastedText.copyAriaLabel": "टाँसिएको पाठ ब्लक प्रतिलिपि गर्नुहोस्",
|
||||
"messageItem.attachment.defaultName": "संलग्नक (attachment)",
|
||||
"messageItem.attachment.downloadAriaLabel": "{name} डाउनलोड गर्नुहोस्",
|
||||
"messageItem.agentMeta.agentLabel": "एजेन्ट: {agent}",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const remoteAccessMessages = {
|
|||
"remoteAccess.addresses.actions.showOther": "अन्य {count} ठेगानाहरू देखाउनुहोस्",
|
||||
"remoteAccess.addresses.actions.hideOther": "अन्य ठेगानाहरू लुकाउनुहोस्",
|
||||
"remoteAccess.address.scope.network": "नेटवर्क",
|
||||
"remoteAccess.address.scope.loopback": "Loopback",
|
||||
"remoteAccess.address.scope.loopback": "लूपब्याक",
|
||||
"remoteAccess.address.scope.internal": "आन्तरिक",
|
||||
"remoteAccess.address.open": "खोल्नुहोस्",
|
||||
"remoteAccess.address.showQr": "QR देखाउनुहोस्",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const sessionMessages = {
|
|||
"sessionList.session.untitled": "शीर्षक नभएको सत्र",
|
||||
"sessionList.status.working": "काम गर्दै",
|
||||
"sessionList.status.compacting": "संक्षिप्त गर्दै",
|
||||
"sessionList.status.idle": "Idle",
|
||||
"sessionList.status.idle": "निष्क्रिय",
|
||||
"sessionList.status.retrying": "पुन: प्रयास गर्दै",
|
||||
"sessionList.status.retryingIn": "{seconds} सेकेन्डमा पुन: प्रयास गर्दै",
|
||||
"sessionList.status.retryTooltip": "{message} (प्रयास {attempt})",
|
||||
|
|
@ -77,8 +77,8 @@ export const sessionMessages = {
|
|||
"sessionView.alerts.revertFailed.title": "फिर्ता गर्न असफल",
|
||||
"sessionView.alerts.deleteUpToFailed.message": "सन्देशहरू मेटाउन असफल भयो",
|
||||
"sessionView.alerts.deleteUpToFailed.title": "मेटाउन असफल",
|
||||
"sessionView.alerts.forkFailed.message": "सत्र Fork गर्न असफल भयो",
|
||||
"sessionView.alerts.forkFailed.title": "Fork असफल भयो",
|
||||
"sessionView.alerts.forkFailed.message": "सत्र फोर्क गर्न असफल भयो",
|
||||
"sessionView.alerts.forkFailed.title": "फोर्क असफल भयो",
|
||||
"sessionView.attachments.expandPastedTextAriaLabel": "टाँसिएको पाठ विस्तार गर्नुहोस्",
|
||||
"sessionView.attachments.insertPastedTextTitle": "टाँसिएको पाठ घुसाउनुहोस्",
|
||||
"sessionView.attachments.removeAriaLabel": "संलग्नक हटाउनुहोस्",
|
||||
|
|
@ -115,7 +115,7 @@ export const sessionMessages = {
|
|||
|
||||
"sessionState.cleanup.deepConfirm.message": "यो सफाई ढिलो हुन सक्छ र तपाईंले नचाहेका सत्रहरू मेटाउन सक्छ। के तपाईं निश्चित हुनुहुन्छ?",
|
||||
"sessionState.cleanup.deepConfirm.title": "सत्रहरूको गहिरो सफाई (Deep Clean)",
|
||||
"sessionState.cleanup.deepConfirm.detail": "गहिरो सफाईले कुनै सन्देश नभएका सबै सत्रहरू, समाप्त भएका उप-एजेन्ट सत्रहरू र प्रयोग नगरिएका Fork हरू मेटाउनेछ।",
|
||||
"sessionState.cleanup.deepConfirm.detail": "गहिरो सफाईले कुनै सन्देश नभएका सबै सत्रहरू, समाप्त भएका उप-एजेन्ट सत्रहरू र प्रयोग नगरिएका फोर्कहरू मेटाउनेछ।",
|
||||
"sessionState.cleanup.deepConfirm.confirmLabel": "जारी राख्नुहोस्",
|
||||
"sessionState.cleanup.deepConfirm.cancelLabel": "रद्द गर्नुहोस्",
|
||||
"sessionState.cleanup.toast.one": "{count} खाली सत्र सफा गरियो",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue