mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 11:05:31 +08:00
Merge branch 'main' into docs/remove-web-command
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
# Enforce LF line endings for all text files
|
||||
* text=auto eol=lf
|
||||
|
||||
# Git LFS tracking for binary/media files
|
||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -5,3 +8,17 @@
|
||||
# Visual regression baseline snapshots
|
||||
packages/kilo-ui/tests/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
packages/kilo-vscode/tests/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Hide non-English localization files by default in GitHub PR diffs.
|
||||
**/i18n/*.ts linguist-generated=true
|
||||
**/i18n/en.ts linguist-generated=false
|
||||
**/i18n/en*.ts linguist-generated=false
|
||||
**/i18n/package-nls-en.ts linguist-generated=false
|
||||
**/i18n/index.ts linguist-generated=false
|
||||
**/i18n/parity.test.ts linguist-generated=false
|
||||
packages/kilo-i18n/src/*.ts linguist-generated=true
|
||||
packages/kilo-i18n/src/en.ts linguist-generated=false
|
||||
|
||||
# Auto-generated CLI reference docs
|
||||
packages/kilo-docs/markdoc/partials/cli-commands-table.md linguist-generated=true
|
||||
packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md linguist-generated=true
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Trigger webhook for feat PRs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
call-webhook:
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
(startsWith(github.event.pull_request.title, 'feat:') ||
|
||||
startsWith(github.event.pull_request.title, 'feat('))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send webhook safely
|
||||
run: |
|
||||
payload=$(jq -n \
|
||||
--arg repo "${GITHUB_REPOSITORY}" \
|
||||
--arg pr_number "${{ github.event.pull_request.number }}" \
|
||||
--arg title "${{ github.event.pull_request.title }}" \
|
||||
--arg body "${{ github.event.pull_request.body }}" \
|
||||
--arg author "${{ github.event.pull_request.user.login }}" \
|
||||
--arg merged_at "${{ github.event.pull_request.merged_at }}" \
|
||||
'{
|
||||
repo: $repo,
|
||||
pr_number: $pr_number,
|
||||
title: $title,
|
||||
description: $body,
|
||||
author: $author,
|
||||
merged_at: $merged_at
|
||||
}'
|
||||
)
|
||||
curl --fail-with-body -X POST "${{ secrets.DOC_WEBHOOK_URL }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-raw "$payload"
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Check opencode annotations
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/opencode/**"
|
||||
- "script/check-opencode-annotations.ts"
|
||||
- ".github/workflows/check-opencode-annotations.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-annotations:
|
||||
name: Check kilocode_change annotations
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Check kilocode_change annotations in shared opencode files
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
if [ -n "$BASE_SHA" ]; then
|
||||
bun run script/check-opencode-annotations.ts --base "$BASE_SHA"
|
||||
else
|
||||
echo "No PR base SHA available (workflow_dispatch without PR context) — skipping."
|
||||
fi
|
||||
@@ -1,5 +1,5 @@
|
||||
name: publish
|
||||
run-name: "${{ format('release {0}', inputs.bump) }}"
|
||||
run-name: "${{ format('{0} {1}', inputs.pre_release && 'pre-release' || 'release', inputs.bump) }}"
|
||||
|
||||
on:
|
||||
# push:
|
||||
@@ -22,6 +22,11 @@ on:
|
||||
description: "Override version (optional)"
|
||||
required: false
|
||||
type: string
|
||||
pre_release:
|
||||
description: "Publish as pre-release (VS Code marketplace)"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
|
||||
|
||||
@@ -117,6 +122,7 @@ jobs:
|
||||
env:
|
||||
CLI_DIST_DIR: ../../packages/opencode/dist
|
||||
KILO_VERSION: ${{ needs.build-cli.outputs.version }}
|
||||
KILO_PRE_RELEASE: ${{ inputs.pre_release }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
@@ -240,11 +246,90 @@ jobs:
|
||||
# APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
# APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||
|
||||
# kilocode_change start
|
||||
# Smoke test disabled as a release gate due to infrastructure issues.
|
||||
# The job is skipped via `if: false` so it no longer blocks publishing.
|
||||
# Re-enable by restoring the original `if:` condition and uncommenting
|
||||
# `- smoke-test` in the publish job's `needs` list below.
|
||||
smoke-test:
|
||||
name: Smoke Test (pre-publish gate) [DISABLED]
|
||||
needs:
|
||||
- version
|
||||
- build-cli
|
||||
if: false # was: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Trigger kilo-bench smoke test
|
||||
id: trigger
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.BENCH_GITHUB_TOKEN }}
|
||||
run: |
|
||||
BEFORE=$(date -u -d '60 seconds ago' +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
gh api repos/Kilo-Org/kilo-bench/dispatches \
|
||||
--method POST \
|
||||
-f event_type=smoke-test \
|
||||
-f 'client_payload[release_tag]=${{ needs.version.outputs.tag }}' \
|
||||
-f 'client_payload[source_run_id]=${{ github.run_id }}'
|
||||
|
||||
# Poll for the run created after our dispatch timestamp.
|
||||
# The dispatch API returns no run ID, so we query by created time
|
||||
# and pick the oldest match to avoid grabbing an unrelated run.
|
||||
echo "Waiting for smoke-test run to appear (dispatched after $BEFORE)..."
|
||||
for attempt in $(seq 1 30); do
|
||||
RUN_ID=$(gh api \
|
||||
"repos/Kilo-Org/kilo-bench/actions/workflows/smoke-test.yml/runs?event=repository_dispatch&created=>=$BEFORE" \
|
||||
--jq '.workflow_runs | sort_by(.created_at) | .[0].id // empty')
|
||||
|
||||
if [[ -n "$RUN_ID" && "$RUN_ID" != "null" ]]; then
|
||||
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::Smoke test run: https://github.com/Kilo-Org/kilo-bench/actions/runs/$RUN_ID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo " attempt $attempt: run not yet registered, retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo "::error::Smoke test run did not appear within 5 minutes after dispatch."
|
||||
exit 1
|
||||
|
||||
- name: Wait for smoke test to complete
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.BENCH_GITHUB_TOKEN }}
|
||||
run: |
|
||||
RUN_ID="${{ steps.trigger.outputs.run_id }}"
|
||||
echo "Waiting for run $RUN_ID..."
|
||||
|
||||
for i in $(seq 1 60); do
|
||||
CONCLUSION=$(gh run view "$RUN_ID" \
|
||||
--repo Kilo-Org/kilo-bench \
|
||||
--json conclusion \
|
||||
--jq '.conclusion')
|
||||
|
||||
echo " attempt $i: $CONCLUSION"
|
||||
|
||||
if [[ "$CONCLUSION" == "success" ]]; then
|
||||
echo "::notice::Smoke test passed."
|
||||
exit 0
|
||||
elif [[ "$CONCLUSION" != "null" && "$CONCLUSION" != "" ]]; then
|
||||
echo "::error::Smoke test failed with conclusion: $CONCLUSION"
|
||||
echo "See: https://github.com/Kilo-Org/kilo-bench/actions/runs/$RUN_ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 30
|
||||
done
|
||||
|
||||
echo "::error::Smoke test did not complete within 30 minutes."
|
||||
exit 1
|
||||
# kilocode_change end
|
||||
publish:
|
||||
needs:
|
||||
- version
|
||||
- build-cli
|
||||
- build-vscode
|
||||
# - smoke-test # disabled: infrastructure issues (see smoke-test job comment)
|
||||
# - build-tauri
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
@@ -315,6 +400,7 @@ jobs:
|
||||
env:
|
||||
KILO_VERSION: ${{ needs.version.outputs.version }}
|
||||
KILO_RELEASE: ${{ needs.version.outputs.release }}
|
||||
KILO_PRE_RELEASE: ${{ inputs.pre_release }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Check Source Links
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/kilo-vscode/src/**"
|
||||
- "packages/kilo-vscode/webview-ui/**"
|
||||
- "packages/opencode/src/**"
|
||||
- "packages/kilo-docs/source-links.md"
|
||||
- "script/extract-source-links.ts"
|
||||
- ".github/workflows/source-check-links.yml"
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
source-links-freshness:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Check source-links.md is up to date
|
||||
run: bun run script/extract-source-links.ts --check
|
||||
@@ -0,0 +1,39 @@
|
||||
name: storybook
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- ".github/workflows/storybook.yml"
|
||||
- "package.json"
|
||||
- "bun.lock"
|
||||
- "packages/storybook/**"
|
||||
- "packages/ui/**"
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- ".github/workflows/storybook.yml"
|
||||
- "package.json"
|
||||
- "bun.lock"
|
||||
- "packages/storybook/**"
|
||||
- "packages/ui/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: false
|
||||
name: storybook build
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Build Storybook
|
||||
run: bun --cwd packages/storybook build
|
||||
@@ -42,3 +42,7 @@ jobs:
|
||||
- name: Check for dead code (knip)
|
||||
working-directory: packages/kilo-vscode
|
||||
run: bun run knip
|
||||
|
||||
- name: Check for kilocode_change markers
|
||||
working-directory: packages/kilo-vscode
|
||||
run: bun run check-kilocode-change
|
||||
|
||||
@@ -25,6 +25,8 @@ jobs:
|
||||
- "packages/kilo-vscode/webview-ui/**"
|
||||
- "packages/kilo-vscode/.storybook/**"
|
||||
- "packages/kilo-vscode/tests/visual-regression*"
|
||||
- "packages/kilo-vscode/tests/permission-dock-dropdown*"
|
||||
- "packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots/**"
|
||||
- ".github/workflows/visual-regression.yml"
|
||||
- name: Check if PR is from a fork
|
||||
id: fork-check
|
||||
@@ -181,7 +183,7 @@ jobs:
|
||||
else
|
||||
git commit -m "chore: update visual regression baselines"
|
||||
git lfs push --all origin
|
||||
git push
|
||||
git push --no-verify
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
@@ -302,10 +304,42 @@ jobs:
|
||||
"
|
||||
working-directory: packages/kilo-vscode
|
||||
|
||||
- name: Remove stale baselines for permission-dock-dropdown tests
|
||||
run: |
|
||||
node -e "
|
||||
const fs = require('fs'), path = require('path');
|
||||
const specPath = 'tests/permission-dock-dropdown.spec.ts';
|
||||
const expected = new Set();
|
||||
if (fs.existsSync(specPath)) {
|
||||
const spec = fs.readFileSync(specPath, 'utf8');
|
||||
const re = /toHaveScreenshot\(\[([^\]]+)\]\)/g;
|
||||
let m;
|
||||
while ((m = re.exec(spec)) !== null) {
|
||||
const parts = m[1].split(',').map(s => s.trim().replace(/[\"']/g, ''));
|
||||
expected.add(parts.join('/').replace(/\.png$/, '-chromium-linux.png'));
|
||||
}
|
||||
}
|
||||
const dir = 'tests/permission-dock-dropdown.spec.ts-snapshots';
|
||||
if (!fs.existsSync(dir)) process.exit(0);
|
||||
for (const sub of fs.readdirSync(dir)) {
|
||||
const sdir = path.join(dir, sub);
|
||||
if (!fs.statSync(sdir).isDirectory()) continue;
|
||||
for (const file of fs.readdirSync(sdir)) {
|
||||
const rel = sub + '/' + file;
|
||||
if (file.endsWith('.png') && !expected.has(rel)) {
|
||||
console.log('Removing stale baseline:', rel);
|
||||
fs.unlinkSync(path.join(sdir, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
working-directory: packages/kilo-vscode
|
||||
|
||||
- name: Check for baseline changes (fork PRs)
|
||||
if: needs.check-paths.outputs.is_fork == 'true'
|
||||
run: |
|
||||
git add packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/
|
||||
[ -d packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots ] && git add packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots/
|
||||
if git diff --cached --quiet; then
|
||||
echo "No visual regression detected."
|
||||
else
|
||||
@@ -320,6 +354,7 @@ jobs:
|
||||
if: needs.check-paths.outputs.is_fork != 'true' && steps.check-baseline-commit-vscode.outputs.is_baseline_update == 'true'
|
||||
run: |
|
||||
git add packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/
|
||||
[ -d packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots ] && git add packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots/
|
||||
if git diff --cached --quiet; then
|
||||
echo "Baselines are stable after auto-update."
|
||||
else
|
||||
@@ -339,13 +374,14 @@ jobs:
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/
|
||||
[ -d packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots ] && git add packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots/
|
||||
if git diff --cached --quiet; then
|
||||
echo "No new baselines — nothing to commit."
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
git commit -m "chore: update kilo-vscode visual regression baselines"
|
||||
git lfs push --all origin
|
||||
git push
|
||||
git push --no-verify
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
|
||||
+4
-1
@@ -4,7 +4,10 @@ node_modules
|
||||
.worktrees
|
||||
.sst
|
||||
.env
|
||||
.idea
|
||||
.idea/*
|
||||
!.idea/gradle.xml
|
||||
!.idea/.run
|
||||
.idea/*.iml
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/launch.json
|
||||
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/kilo-jetbrains" />
|
||||
<option name="gradleJvm" value="#JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains" />
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/backend" />
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/frontend" />
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/shared" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -59,7 +59,7 @@ export const MyStoryName: Story = {
|
||||
name: "MyComponent — description of variant",
|
||||
render: () => (
|
||||
<StoryProviders>
|
||||
<div style={{ width: "420px", height: "400px" }}>
|
||||
<div style={{ "max-height": "400px", overflow: "auto" }}>
|
||||
<MyComponent someProp="value" />
|
||||
</div>
|
||||
</StoryProviders>
|
||||
@@ -71,7 +71,8 @@ export const MyStoryName: Story = {
|
||||
|
||||
- **Always start with `/** @jsxImportSource solid-js \*/`\*\* — required for SolidJS JSX compilation.
|
||||
- **Always wrap in `<StoryProviders>`** — provides all required contexts (VSCode, Server, Config, Provider, Session, I18n, Dialog, Marked, Data, Diff, Code). Without it, components that call `useVSCode()`, `useSession()`, etc. will throw.
|
||||
- **Set explicit dimensions** on a wrapper div so the screenshot has a deterministic size. Use `width: "420px"` for sidebar-width components (the standard VS Code sidebar). Use `width: "200px"` for narrow/collapsed states.
|
||||
- **Do NOT set an explicit `width` on the wrapper div.** The Playwright viewport is already 420px wide (or 200px for narrow stories). Setting `width: "420px"` leaves no room for a vertical scrollbar and causes right-side cropping in screenshots. Let the viewport control the width.
|
||||
- **Use `max-height` not `height` for the wrapper div** when you need to constrain vertical size. A fixed `height` forces a scrollbar even when content is short; `max-height` avoids unnecessary scrollbars that would eat into the available horizontal space.
|
||||
- **Meta `title`** determines the snapshot subdirectory. Use PascalCase or slash-notation (e.g., `"Composite/Webview"`). Playwright transforms it: `"Composite/Webview"` becomes `composite-webview/` in the snapshots folder.
|
||||
- **Export name** determines the story ID. The Storybook ID is `{lowercase-title}--{kebab-export-name}`. For example, `title: "Chat"` + `export const ChatViewIdle` produces ID `chat--chat-view-idle`.
|
||||
- **Snapshot path** is derived automatically: `tests/visual-regression.spec.ts-snapshots/{title-slug}/{variant-slug}.png`. Example: `chat/chat-view-idle-chromium-linux.png`.
|
||||
|
||||
@@ -3,7 +3,7 @@ description: "find issue(s) on github"
|
||||
model: opencode/claude-haiku-4-5
|
||||
---
|
||||
|
||||
Search through existing issues in Kilo-Org/kilo using the gh cli to find issues matching this query:
|
||||
Search through existing issues in Kilo-Org/kilocode using the gh cli to find issues matching this query:
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
"options": {},
|
||||
},
|
||||
},
|
||||
"permission": {
|
||||
"edit": {
|
||||
"packages/opencode/migration/*": "deny",
|
||||
},
|
||||
},
|
||||
"mcp": {},
|
||||
"tools": {
|
||||
"github-triage": false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Use this tool to search GitHub pull requests by title and description.
|
||||
|
||||
This tool searches PRs in the sst/opencode repository and returns LLM-friendly results including:
|
||||
This tool searches PRs in the anomalyco/opencode repository and returns LLM-friendly results including:
|
||||
- PR number and title
|
||||
- Author
|
||||
- State (open/closed/merged)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run JetBrains Plugin" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/kilo-jetbrains" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="runIde" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<GradleProfilingDisabled>false</GradleProfilingDisabled>
|
||||
<GradleCoverageDisabled>false</GradleCoverageDisabled>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
Vendored
+35
-2
@@ -65,6 +65,20 @@
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "VSCode - Install",
|
||||
"type": "shell",
|
||||
"command": "bun",
|
||||
"args": ["install", "--frozen-lockfile"],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "VSCode - Compile",
|
||||
"type": "shell",
|
||||
@@ -77,6 +91,7 @@
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/packages/kilo-vscode"
|
||||
},
|
||||
"dependsOn": ["VSCode - Install"],
|
||||
"problemMatcher": ["$tsc", "$eslint-stylish"]
|
||||
},
|
||||
{
|
||||
@@ -101,9 +116,27 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "install-dev-extension",
|
||||
"label": "VSCode - DevSnapshot - Build",
|
||||
"type": "shell",
|
||||
"command": "bun script/install-dev-extension.ts",
|
||||
"command": "bun run snapshot:build",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/packages/kilo-vscode"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "VSCode - DevSnapshot - Install",
|
||||
"type": "shell",
|
||||
"command": "bun run snapshot:install",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
|
||||
@@ -10,11 +10,16 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang
|
||||
## Build and Dev
|
||||
|
||||
- **Dev**: `bun run dev` (runs from root) or `bun run --cwd packages/opencode --conditions=browser src/index.ts`
|
||||
- **Dev with params**: `bun dev -- help`
|
||||
- **Extension**: `bun run extension` (build + launch VS Code with the extension in dev mode). Pass `--no-build` to skip the build.
|
||||
- **Typecheck**: `bun turbo typecheck` (uses `tsgo`, not `tsc`)
|
||||
- **Test**: `bun test` from `packages/opencode/` (NOT from root -- root blocks tests)
|
||||
- **Single test**: `bun test test/tool/tool.test.ts` from `packages/opencode/`
|
||||
- **SDK regen**: After changing server endpoints in `packages/opencode/src/server/`, run `./script/generate.ts` from root to regenerate `packages/sdk/js/`
|
||||
- **Knip** (unused exports): `bun run knip` from `packages/kilo-vscode/`. CI runs this — all exported types/functions must be imported somewhere. Remove or unexport unused exports before pushing.
|
||||
- **Source links**: After adding or changing URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`, run `bun run script/extract-source-links.ts` from the repo root and commit the updated `packages/kilo-docs/source-links.md`. CI runs this check — the build fails if the file is stale.
|
||||
- **kilocode_change check**: `bun run check-kilocode-change` from `packages/kilo-vscode/`. CI runs this — `kilocode_change` is a marker for upstream merge conflicts and must not appear in `packages/kilo-vscode/` or `packages/kilo-ui/` (these are entirely Kilo Code additions). Remove the markers before pushing.
|
||||
- **opencode annotation check**: `bun run script/check-opencode-annotations.ts` from repo root. CI runs this on PRs touching `packages/opencode/` — every Kilo-specific change in shared opencode files must be annotated with `kilocode_change` markers. Exempt paths (no markers needed): `packages/opencode/src/kilocode/`, `packages/opencode/test/kilocode/`, and any path containing `kilocode` in the name.
|
||||
|
||||
## Products
|
||||
|
||||
@@ -162,10 +167,16 @@ const bazFoo = 3
|
||||
You MUST avoid using `mocks` as much as possible.
|
||||
Tests MUST test actual implementation, do not duplicate logic into a test.
|
||||
|
||||
## Commit Conventions
|
||||
|
||||
[Conventional Commits](https://www.conventionalcommits.org/) with scopes matching packages: `vscode`, `cli`, `agent-manager`, `sdk`, `ui`, `i18n`, `kilo-docs`, `gateway`, `telemetry`, `desktop`. Omit scope when spanning multiple packages.
|
||||
|
||||
## Fork Merge Process
|
||||
|
||||
Kilo CLI is a fork of [opencode](https://github.com/anomalyco/opencode).
|
||||
|
||||
**Very important**: when planning or coding, update shared files with OpenCode as last resort! Everything is shared code from OpenCode, except folders that contain `kilo` in the name or have a parent directory that contains `kilo` in the name. Example of kilo specific folders: `packages/opencode/src/kilocode/` and `packages/kilo-docs/`. Always look for ways to implement your feature or fix in a way that minimizes changes to shared code.
|
||||
|
||||
### Minimizing Merge Conflicts
|
||||
|
||||
We regularly merge upstream changes from opencode. To minimize merge conflicts and keep the sync process smooth:
|
||||
|
||||
@@ -24,6 +24,16 @@ The Kilo Community is [on Discord](https://kilo.ai/discord).
|
||||
bun dev
|
||||
```
|
||||
|
||||
### Developing the VS Code Extension
|
||||
|
||||
Build and launch the extension in an isolated VS Code instance:
|
||||
|
||||
```bash
|
||||
bun run extension # Build + launch in dev mode
|
||||
```
|
||||
|
||||
This auto-detects VS Code on macOS, Linux, and Windows. Override with `--app-path PATH` or `VSCODE_EXEC_PATH`. Use `--insiders` to prefer Insiders, `--workspace PATH` to open a specific folder, or `--clean` to reset cached state.
|
||||
|
||||
### Running against a different directory
|
||||
|
||||
By default, `bun dev` runs Kilo CLI in the `packages/opencode` directory. To run it against a different directory or repository:
|
||||
@@ -68,6 +78,26 @@ kilo --help # Show all available commands
|
||||
kilo serve # Start headless API server
|
||||
```
|
||||
|
||||
### Testing with a local backend
|
||||
|
||||
To point the CLI at a local backend (e.g., a locally running Kilo API server on port 3000), set the `KILO_API_URL` environment variable:
|
||||
|
||||
```bash
|
||||
KILO_API_URL=http://localhost:3000 bun dev
|
||||
```
|
||||
|
||||
This redirects all gateway traffic (auth, model listing, provider routing, profile, etc.) to your local server. The default is `https://api.kilo.ai`.
|
||||
|
||||
There are also optional overrides for other services:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
| ------------------------- | -------------------------------- | ----------------------------------------- |
|
||||
| `KILO_API_URL` | `https://api.kilo.ai` | Kilo API (gateway, auth, models, profile) |
|
||||
| `KILO_SESSION_INGEST_URL` | `https://ingest.kilosessions.ai` | Session export / cloud sync |
|
||||
| `KILO_MODELS_URL` | `https://models.dev` | Model metadata |
|
||||
|
||||
> **VS Code:** The repo includes a "VSCode - Run Extension (Local Backend)" launch config in `.vscode/launch.json` that sets `KILO_API_URL=http://localhost:3000` automatically.
|
||||
|
||||
### Pull Request Expectations
|
||||
|
||||
- **Issue First Policy:** All PRs must reference an existing issue.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<p align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
|
||||
<a href="https://x.com/kilocode"><img src="https://img.shields.io/badge/kilocode-000000?style=flat&logo=x&logoColor=white" alt="X (Twitter)"></a>
|
||||
<a href="https://blog.kilo.ai"><img src="https://img.shields.io/badge/Blog-555?style=flat&logo=substack&logoColor=white" alt="Substack Blog"></a>
|
||||
<a href="https://kilo.ai/discord"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
|
||||
<a href="https://www.reddit.com/r/kilocode/"><img src="https://img.shields.io/badge/Join%20r%2Fkilocode-D84315?style=flat&logo=reddit&logoColor=white" alt="Reddit"></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code"><img src="https://raster.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace" height="20"></a>
|
||||
<a href="https://x.com/kilocode"><img src="https://raster.shields.io/badge/kilocode-000000?style=flat&logo=x&logoColor=white" alt="X (Twitter)" height="20"></a>
|
||||
<a href="https://blog.kilo.ai"><img src="https://raster.shields.io/badge/Blog-555?style=flat&logo=substack&logoColor=white" alt="Substack Blog" height="20"></a>
|
||||
<a href="https://kilo.ai/discord"><img src="https://raster.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" height="20"></a>
|
||||
<a href="https://www.reddit.com/r/kilocode/"><img src="https://raster.shields.io/badge/Join%20r%2Fkilocode-D84315?style=flat&logo=reddit&logoColor=white" alt="Reddit" height="20"></a>
|
||||
</p>
|
||||
|
||||
# 🚀 Kilo
|
||||
@@ -18,7 +18,7 @@
|
||||
- ⚡ Inline autocomplete suggestions
|
||||
- 🤖 Latest AI models
|
||||
- 🎁 API keys optional
|
||||
- 💡 **Get $20 in bonus credits when you top-up for the first time** Credits can be used with 500+ models like Gemini 3.1 Pro, Claude 4.6 Sonnet & Opus, and GPT-5.2
|
||||
- 💡 **Get $20 in bonus credits when you top-up for the first time** Credits can be used with 500+ models like Gemini 3.1 Pro, Claude 4.6 Sonnet & Opus, and GPT-5.4
|
||||
|
||||
## Quick Links
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
## Get Started in Visual Studio Code
|
||||
|
||||
1. Install the Kilo Code extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code).
|
||||
2. Create your account to access 500+ cutting-edge AI models including Gemini 3 Pro, Claude 4.5 Sonnet & Opus, and GPT-5 – with transparent pricing that matches provider rates exactly.
|
||||
2. Create your account to access 500+ cutting-edge AI models including Gemini 3.1 Pro, Claude 4.6 Sonnet & Opus, and GPT-5.4 – with transparent pricing that matches provider rates exactly.
|
||||
3. Start coding with AI that adapts to your workflow. Watch our quick-start guide to see Kilo in action:
|
||||
|
||||
[](https://youtu.be/pqGfYXgrhig)
|
||||
<a href="https://youtu.be/pqGfYXgrhig"><img src="https://img.youtube.com/vi/pqGfYXgrhig/maxresdefault.jpg" alt="Watch the video" width="640" height="360"></a>
|
||||
|
||||
## Get Started with the CLI
|
||||
|
||||
@@ -84,7 +84,7 @@ For most users:
|
||||
- **Windows (most PCs):** `kilo-windows-x64.zip`
|
||||
- **macOS Apple Silicon:** `kilo-darwin-arm64.zip`
|
||||
- **macOS Intel:** `kilo-darwin-x64.zip`
|
||||
- **Linux x64:** `kilo-linux-x64.tar,gz`
|
||||
- **Linux x64:** `kilo-linux-x64.tar.gz`
|
||||
- **Linux on ARM:** `kilo-linux-arm64.tar.gz`
|
||||
|
||||
### Autonomous Mode (CI/CD)
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@ The `publish.yml` workflow runs four jobs sequentially:
|
||||
- Builds native binaries for **all supported platforms and architectures**:
|
||||
- Linux: x64, arm64 (glibc and musl), plus baseline (non-AVX2) variants
|
||||
- macOS: x64, arm64, plus baseline variants
|
||||
- Windows: x64, plus baseline variant
|
||||
- Windows: x64 (plus baseline variant), arm64
|
||||
- Patches ELF interpreters on Linux binaries for broad compatibility.
|
||||
- Creates platform archives (`.tar.gz` for Linux, `.zip` for macOS/Windows) and uploads them to the draft GitHub Release.
|
||||
- Uploads the `dist/` directory as a workflow artifact (`kilo-cli`) for subsequent jobs.
|
||||
@@ -43,7 +43,7 @@ The `publish.yml` workflow runs four jobs sequentially:
|
||||
|
||||
- Downloads the CLI artifacts from the previous job.
|
||||
- Runs `packages/kilo-vscode/script/build.ts` to build VSIX packages for all target platforms:
|
||||
- `linux-x64`, `linux-arm64`, `alpine-x64`, `alpine-arm64`, `darwin-x64`, `darwin-arm64`, `win32-x64`
|
||||
- `linux-x64`, `linux-arm64`, `alpine-x64`, `alpine-arm64`, `darwin-x64`, `darwin-arm64`, `win32-x64`, `win32-arm64`
|
||||
- Each VSIX bundles the platform-specific CLI binary.
|
||||
- Uploads the VSIX files as a workflow artifact (`kilo-vscode`).
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1772956932,
|
||||
"narHash": "sha256-M0yS4AafhKxPPmOHGqIV0iKxgNO8bHDWdl1kOwGBwRY=",
|
||||
"lastModified": 1773628058,
|
||||
"narHash": "sha256-hpXH0z3K9xv0fHaje136KY872VT2T5uwxtezlAskQgY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "608d0cadfed240589a7eea422407a547ad626a14",
|
||||
"rev": "f8573b9c935cfaa162dd62cc9e75ae2db86f85df",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -150,28 +150,54 @@
|
||||
'';
|
||||
in
|
||||
pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
nodejs_20
|
||||
pkg-config
|
||||
openssl
|
||||
git
|
||||
gh
|
||||
playwright-driver.browsers
|
||||
vsce
|
||||
unzip
|
||||
gnutar
|
||||
gzip
|
||||
patchelf
|
||||
ripgrep
|
||||
kilo-dev
|
||||
kilo-install-bin
|
||||
kilo-bin
|
||||
];
|
||||
packages =
|
||||
with pkgs;
|
||||
[
|
||||
bun
|
||||
nodejs_20
|
||||
pkg-config
|
||||
openssl
|
||||
git
|
||||
gh
|
||||
playwright-driver.browsers
|
||||
vsce
|
||||
unzip
|
||||
gnutar
|
||||
gzip
|
||||
patchelf
|
||||
ripgrep
|
||||
jetbrains.jdk
|
||||
jdk21
|
||||
kilo-dev
|
||||
kilo-install-bin
|
||||
kilo-bin
|
||||
]
|
||||
++ lib.optionals stdenv.isLinux [
|
||||
libX11
|
||||
libXext
|
||||
libXrender
|
||||
libXtst
|
||||
libXi
|
||||
fontconfig
|
||||
freetype
|
||||
];
|
||||
shellHook = ''
|
||||
export KILO_ROOT="$PWD"
|
||||
export PLAYWRIGHT_BROWSERS_PATH="${pkgs.playwright-driver.browsers}"
|
||||
export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true
|
||||
''
|
||||
+ pkgs.lib.optionalString pkgs.stdenv.isLinux ''
|
||||
export LD_LIBRARY_PATH="${
|
||||
pkgs.lib.makeLibraryPath [
|
||||
pkgs.libX11
|
||||
pkgs.libXext
|
||||
pkgs.libXrender
|
||||
pkgs.libXtst
|
||||
pkgs.libXi
|
||||
pkgs.fontconfig
|
||||
pkgs.freetype
|
||||
]
|
||||
}:$LD_LIBRARY_PATH"
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-2SnZbJPIvgCkb4jNh49CnbDAOpghgO9XihSiTlL6rYI=",
|
||||
"aarch64-linux": "sha256-HV/Sm4rBAZPjxG+SQJ95634yTyJXDjlSeYemMYg8eCE=",
|
||||
"aarch64-darwin": "sha256-PAJSd1VkNc7LUSrOAeG+rmYUw8VefR8GfkAIy8H3iV0=",
|
||||
"x86_64-darwin": "sha256-Qdk8nu4FoGjp6+7Ji5vuHw2HhM1Z+ivE7meqJivTiHk="
|
||||
"x86_64-linux": "sha256-PvqhO2tOClenS37A80VsNuOglBs5fk8aaAtsktwG74s=",
|
||||
"aarch64-linux": "sha256-+UN4dTY2s7GWjYumPoKTSv2pX7zhJB51hxJzCwqejaE=",
|
||||
"aarch64-darwin": "sha256-N5JmmR6XrzGT86AL6TxpcQoZl7PTmgfd6ll5CVamQT0=",
|
||||
"x86_64-darwin": "sha256-eyLUps3Kzg+6/VPt/XW78+vaL+9RY1aTtnnPxUPSkWQ="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ stdenvNoCC.mkDerivation {
|
||||
../package.json
|
||||
../patches
|
||||
../install # required by desktop build (cli.rs include_str!)
|
||||
../.github/TEAM_MEMBERS # required by @opencode-ai/script
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
+27
-14
@@ -14,7 +14,8 @@
|
||||
"prepare": "husky",
|
||||
"random": "echo 'Random script'",
|
||||
"hello": "echo 'Hello World!'",
|
||||
"test": "echo 'do not run tests from root' && exit 1"
|
||||
"test": "echo 'do not run tests from root' && exit 1",
|
||||
"extension": "bun --cwd packages/kilo-vscode script/launch.ts"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -37,12 +38,12 @@
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-beta.12-a5629fb",
|
||||
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
||||
"diff": "8.0.4",
|
||||
"dompurify": "3.3.3",
|
||||
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
||||
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
||||
"ai": "5.0.124",
|
||||
"hono": "4.10.7",
|
||||
"hono": "4.12.12",
|
||||
"hono-openapi": "1.1.2",
|
||||
"fuzzysort": "3.1.0",
|
||||
"luxon": "3.6.1",
|
||||
@@ -50,18 +51,18 @@
|
||||
"marked-shiki": "1.2.1",
|
||||
"@playwright/test": "1.51.0",
|
||||
"typescript": "5.8.2",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260316.1",
|
||||
"zod": "4.1.8",
|
||||
"remeda": "2.26.0",
|
||||
"shiki": "3.20.0",
|
||||
"solid-list": "0.3.0",
|
||||
"tailwindcss": "4.1.11",
|
||||
"virtua": "0.42.3",
|
||||
"vite": "7.1.4",
|
||||
"vite": "7.3.2",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
"@solidjs/router": "0.15.4",
|
||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||
"solid-js": "1.9.10",
|
||||
"solid-js": "1.9.12",
|
||||
"vite-plugin-solid": "2.11.10"
|
||||
}
|
||||
},
|
||||
@@ -69,19 +70,21 @@
|
||||
"@actions/artifact": "5.0.1",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"glob": "13.0.5",
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "3.18.10",
|
||||
"turbo": "2.5.6"
|
||||
"turbo": "2.8.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@aws-sdk/client-s3": "3.1025.0",
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"typescript": "catalog:"
|
||||
"typescript": "catalog:",
|
||||
"@morphllm/morphsdk": "0.2.141"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -102,13 +105,23 @@
|
||||
],
|
||||
"overrides": {
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:"
|
||||
"@types/node": "catalog:",
|
||||
"path-to-regexp": ">=8.4.0",
|
||||
"picomatch": ">=2.3.2",
|
||||
"defu": "6.1.6",
|
||||
"lodash": "4.18.1",
|
||||
"@xmldom/xmldom": ">=0.8.12",
|
||||
"smol-toml": ">=1.6.1",
|
||||
"fastify": ">=5.8.3",
|
||||
"diff": "8.0.4",
|
||||
"dompurify": "3.3.3",
|
||||
"happy-dom": ">=20.8.9"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch",
|
||||
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
|
||||
},
|
||||
"version": "7.0.47",
|
||||
"version": "7.2.0",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,12 @@ test("test description", async ({ page, sdk, gotoSession }) => {
|
||||
- `closeDialog(page, dialog)` - Close any dialog
|
||||
- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
|
||||
- `withSession(sdk, title, callback)` - Create temp session
|
||||
- `withProject(...)` - Create temp project/workspace
|
||||
- `sessionIDFromUrl(url)` - Read session ID from URL
|
||||
- `slugFromUrl(url)` - Read workspace slug from URL
|
||||
- `waitSlug(page, skip?)` - Wait for resolved workspace slug
|
||||
- `trackSession(sessionID, directory?)` - Register session for fixture cleanup
|
||||
- `trackDirectory(directory)` - Register directory for fixture cleanup
|
||||
- `clickListItem(container, filter)` - Click list item by key/text
|
||||
|
||||
**Selectors** (`selectors.ts`):
|
||||
@@ -109,7 +115,7 @@ import { test, expect } from "@playwright/test"
|
||||
|
||||
### Error Handling
|
||||
|
||||
Tests should clean up after themselves:
|
||||
Tests should clean up after themselves. Prefer fixture-managed cleanup:
|
||||
|
||||
```typescript
|
||||
test("test with cleanup", async ({ page, sdk, gotoSession }) => {
|
||||
@@ -120,6 +126,11 @@ test("test with cleanup", async ({ page, sdk, gotoSession }) => {
|
||||
})
|
||||
```
|
||||
|
||||
- Prefer `withSession(...)` for temp sessions
|
||||
- In `withProject(...)` tests that create sessions or extra workspaces, call `trackSession(sessionID, directory?)` and `trackDirectory(directory)`
|
||||
- This lets fixture teardown abort, wait for idle, and clean up safely under CI concurrency
|
||||
- Avoid calling `sdk.session.delete(...)` directly
|
||||
|
||||
### Timeouts
|
||||
|
||||
Default: 60s per test, 10s per assertion. Override when needed:
|
||||
@@ -161,9 +172,10 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings
|
||||
1. Choose appropriate folder or create new one
|
||||
2. Import from `../fixtures`
|
||||
3. Use helper functions from `../actions` and `../selectors`
|
||||
4. Clean up any created resources
|
||||
5. Use specific selectors (avoid CSS classes)
|
||||
6. Test one feature per test file
|
||||
4. When validating routing, use shared helpers from `../actions`. Workspace URL slugs can be canonicalized on Windows, so assert against canonical or resolved workspace slugs.
|
||||
5. Clean up any created resources
|
||||
6. Use specific selectors (avoid CSS classes)
|
||||
7. Test one feature per test file
|
||||
|
||||
## Local Development
|
||||
|
||||
|
||||
+185
-51
@@ -3,12 +3,12 @@ import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { execSync } from "node:child_process"
|
||||
import { modKey, serverUrl } from "./utils"
|
||||
import { createSdk, modKey, resolveDirectory, serverUrl } from "./utils"
|
||||
import {
|
||||
sessionItemSelector,
|
||||
dropdownMenuTriggerSelector,
|
||||
dropdownMenuContentSelector,
|
||||
projectMenuTriggerSelector,
|
||||
projectCloseMenuSelector,
|
||||
projectWorkspacesToggleSelector,
|
||||
titlebarRightSelector,
|
||||
popoverBodySelector,
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
workspaceItemSelector,
|
||||
workspaceMenuTriggerSelector,
|
||||
} from "./selectors"
|
||||
import type { createSdk } from "./utils"
|
||||
|
||||
export async function defocus(page: Page) {
|
||||
await page
|
||||
@@ -61,9 +60,9 @@ export async function closeDialog(page: Page, dialog: Locator) {
|
||||
}
|
||||
|
||||
export async function isSidebarClosed(page: Page) {
|
||||
const main = page.locator("main")
|
||||
const classes = (await main.getAttribute("class")) ?? ""
|
||||
return classes.includes("xl:border-l")
|
||||
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||
await expect(button).toBeVisible()
|
||||
return (await button.getAttribute("aria-expanded")) !== "true"
|
||||
}
|
||||
|
||||
export async function toggleSidebar(page: Page) {
|
||||
@@ -75,48 +74,34 @@ export async function openSidebar(page: Page) {
|
||||
if (!(await isSidebarClosed(page))) return
|
||||
|
||||
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||
const visible = await button
|
||||
.isVisible()
|
||||
.then((x) => x)
|
||||
.catch(() => false)
|
||||
await button.click()
|
||||
|
||||
if (visible) await button.click()
|
||||
if (!visible) await toggleSidebar(page)
|
||||
|
||||
const main = page.locator("main")
|
||||
const opened = await expect(main)
|
||||
.not.toHaveClass(/xl:border-l/, { timeout: 1500 })
|
||||
const opened = await expect(button)
|
||||
.toHaveAttribute("aria-expanded", "true", { timeout: 1500 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (opened) return
|
||||
|
||||
await toggleSidebar(page)
|
||||
await expect(main).not.toHaveClass(/xl:border-l/)
|
||||
await expect(button).toHaveAttribute("aria-expanded", "true")
|
||||
}
|
||||
|
||||
export async function closeSidebar(page: Page) {
|
||||
if (await isSidebarClosed(page)) return
|
||||
|
||||
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||
const visible = await button
|
||||
.isVisible()
|
||||
.then((x) => x)
|
||||
.catch(() => false)
|
||||
await button.click()
|
||||
|
||||
if (visible) await button.click()
|
||||
if (!visible) await toggleSidebar(page)
|
||||
|
||||
const main = page.locator("main")
|
||||
const closed = await expect(main)
|
||||
.toHaveClass(/xl:border-l/, { timeout: 1500 })
|
||||
const closed = await expect(button)
|
||||
.toHaveAttribute("aria-expanded", "false", { timeout: 1500 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (closed) return
|
||||
|
||||
await toggleSidebar(page)
|
||||
await expect(main).toHaveClass(/xl:border-l/)
|
||||
await expect(button).toHaveAttribute("aria-expanded", "false")
|
||||
}
|
||||
|
||||
export async function openSettings(page: Page) {
|
||||
@@ -197,17 +182,48 @@ export async function createTestProject() {
|
||||
await fs.writeFile(path.join(root, "README.md"), "# e2e\n")
|
||||
|
||||
execSync("git init", { cwd: root, stdio: "ignore" })
|
||||
execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" })
|
||||
execSync("git add -A", { cwd: root, stdio: "ignore" })
|
||||
execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', {
|
||||
cwd: root,
|
||||
stdio: "ignore",
|
||||
})
|
||||
|
||||
return root
|
||||
return resolveDirectory(root)
|
||||
}
|
||||
|
||||
export async function cleanupTestProject(directory: string) {
|
||||
await fs.rm(directory, { recursive: true, force: true }).catch(() => undefined)
|
||||
try {
|
||||
execSync("git fsmonitor--daemon stop", { cwd: directory, stdio: "ignore" })
|
||||
} catch {}
|
||||
await fs.rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => undefined)
|
||||
}
|
||||
|
||||
export function slugFromUrl(url: string) {
|
||||
return /\/([^/]+)\/session(?:[/?#]|$)/.exec(url)?.[1] ?? ""
|
||||
}
|
||||
|
||||
export async function waitSlug(page: Page, skip: string[] = []) {
|
||||
let prev = ""
|
||||
let next = ""
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
const slug = slugFromUrl(page.url())
|
||||
if (!slug) return ""
|
||||
if (skip.includes(slug)) return ""
|
||||
if (slug !== prev) {
|
||||
prev = slug
|
||||
next = ""
|
||||
return ""
|
||||
}
|
||||
next = slug
|
||||
return slug
|
||||
},
|
||||
{ timeout: 45_000 },
|
||||
)
|
||||
.not.toBe("")
|
||||
return next
|
||||
}
|
||||
|
||||
export function sessionIDFromUrl(url: string) {
|
||||
@@ -216,7 +232,7 @@ export function sessionIDFromUrl(url: string) {
|
||||
}
|
||||
|
||||
export async function hoverSessionItem(page: Page, sessionID: string) {
|
||||
const sessionEl = page.locator(sessionItemSelector(sessionID)).first()
|
||||
const sessionEl = page.locator(`[data-session-id="${sessionID}"]`).last()
|
||||
await expect(sessionEl).toBeVisible()
|
||||
await sessionEl.hover()
|
||||
return sessionEl
|
||||
@@ -320,6 +336,57 @@ export async function clickListItem(
|
||||
return item
|
||||
}
|
||||
|
||||
async function status(sdk: ReturnType<typeof createSdk>, sessionID: string) {
|
||||
const data = await sdk.session
|
||||
.status()
|
||||
.then((x) => x.data ?? {})
|
||||
.catch(() => undefined)
|
||||
return data?.[sessionID]
|
||||
}
|
||||
|
||||
async function stable(sdk: ReturnType<typeof createSdk>, sessionID: string, timeout = 10_000) {
|
||||
let prev = ""
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const info = await sdk.session
|
||||
.get({ sessionID })
|
||||
.then((x) => x.data)
|
||||
.catch(() => undefined)
|
||||
if (!info) return true
|
||||
const next = `${info.title}:${info.time.updated ?? info.time.created}`
|
||||
if (next !== prev) {
|
||||
prev = next
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
{ timeout },
|
||||
)
|
||||
.toBe(true)
|
||||
}
|
||||
|
||||
export async function waitSessionIdle(sdk: ReturnType<typeof createSdk>, sessionID: string, timeout = 30_000) {
|
||||
await expect.poll(() => status(sdk, sessionID).then((x) => !x || x.type === "idle"), { timeout }).toBe(true)
|
||||
}
|
||||
|
||||
export async function cleanupSession(input: {
|
||||
sessionID: string
|
||||
directory?: string
|
||||
sdk?: ReturnType<typeof createSdk>
|
||||
}) {
|
||||
const sdk = input.sdk ?? (input.directory ? createSdk(input.directory) : undefined)
|
||||
if (!sdk) throw new Error("cleanupSession requires sdk or directory")
|
||||
await waitSessionIdle(sdk, input.sessionID, 5_000).catch(() => undefined)
|
||||
const current = await status(sdk, input.sessionID).catch(() => undefined)
|
||||
if (current && current.type !== "idle") {
|
||||
await sdk.session.abort({ sessionID: input.sessionID }).catch(() => undefined)
|
||||
await waitSessionIdle(sdk, input.sessionID).catch(() => undefined)
|
||||
}
|
||||
await stable(sdk, input.sessionID).catch(() => undefined)
|
||||
await sdk.session.delete({ sessionID: input.sessionID }).catch(() => undefined)
|
||||
}
|
||||
|
||||
export async function withSession<T>(
|
||||
sdk: ReturnType<typeof createSdk>,
|
||||
title: string,
|
||||
@@ -331,7 +398,7 @@ export async function withSession<T>(
|
||||
try {
|
||||
return await callback(session)
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
|
||||
await cleanupSession({ sdk, sessionID: session.id })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,6 +511,57 @@ export async function seedSessionPermission(
|
||||
return { id: result.id }
|
||||
}
|
||||
|
||||
export async function seedSessionTask(
|
||||
sdk: ReturnType<typeof createSdk>,
|
||||
input: {
|
||||
sessionID: string
|
||||
description: string
|
||||
prompt: string
|
||||
subagentType?: string
|
||||
},
|
||||
) {
|
||||
const text = [
|
||||
"Your only valid response is one task tool call.",
|
||||
`Use this JSON input: ${JSON.stringify({
|
||||
description: input.description,
|
||||
prompt: input.prompt,
|
||||
subagent_type: input.subagentType ?? "general",
|
||||
})}`,
|
||||
"Do not output plain text.",
|
||||
"Wait for the task to start and return the child session id.",
|
||||
].join("\n")
|
||||
|
||||
const result = await seed({
|
||||
sdk,
|
||||
sessionID: input.sessionID,
|
||||
prompt: text,
|
||||
timeout: 90_000,
|
||||
probe: async () => {
|
||||
const messages = await sdk.session.messages({ sessionID: input.sessionID, limit: 50 }).then((x) => x.data ?? [])
|
||||
const part = messages
|
||||
.flatMap((message) => message.parts)
|
||||
.find((part) => {
|
||||
if (part.type !== "tool" || part.tool !== "task") return false
|
||||
if (part.state.input?.description !== input.description) return false
|
||||
return typeof part.state.metadata?.sessionId === "string" && part.state.metadata.sessionId.length > 0
|
||||
})
|
||||
|
||||
if (!part) return
|
||||
const id = part.state.metadata?.sessionId
|
||||
if (typeof id !== "string" || !id) return
|
||||
const child = await sdk.session
|
||||
.get({ sessionID: id })
|
||||
.then((x) => x.data)
|
||||
.catch(() => undefined)
|
||||
if (!child?.id) return
|
||||
return { sessionID: id }
|
||||
},
|
||||
})
|
||||
|
||||
if (!result) throw new Error("Timed out seeding task tool")
|
||||
return result
|
||||
}
|
||||
|
||||
export async function seedSessionTodos(
|
||||
sdk: ReturnType<typeof createSdk>,
|
||||
input: {
|
||||
@@ -518,32 +636,42 @@ export async function openProjectMenu(page: Page, projectSlug: string) {
|
||||
const trigger = page.locator(projectMenuTriggerSelector(projectSlug)).first()
|
||||
await expect(trigger).toHaveCount(1)
|
||||
|
||||
const menu = page
|
||||
.locator(dropdownMenuContentSelector)
|
||||
.filter({ has: page.locator(projectCloseMenuSelector(projectSlug)) })
|
||||
.first()
|
||||
const close = menu.locator(projectCloseMenuSelector(projectSlug)).first()
|
||||
|
||||
const clicked = await trigger
|
||||
.click({ timeout: 1500 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (clicked) {
|
||||
const opened = await menu
|
||||
.waitFor({ state: "visible", timeout: 1500 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
if (opened) {
|
||||
await expect(close).toBeVisible()
|
||||
return menu
|
||||
}
|
||||
}
|
||||
|
||||
await trigger.focus()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
const menu = page.locator(dropdownMenuContentSelector).first()
|
||||
const opened = await menu
|
||||
.waitFor({ state: "visible", timeout: 1500 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (opened) {
|
||||
const viewport = page.viewportSize()
|
||||
const x = viewport ? Math.max(viewport.width - 5, 0) : 1200
|
||||
const y = viewport ? Math.max(viewport.height - 5, 0) : 800
|
||||
await page.mouse.move(x, y)
|
||||
await expect(close).toBeVisible()
|
||||
return menu
|
||||
}
|
||||
|
||||
await trigger.click({ force: true })
|
||||
|
||||
await expect(menu).toBeVisible()
|
||||
|
||||
const viewport = page.viewportSize()
|
||||
const x = viewport ? Math.max(viewport.width - 5, 0) : 1200
|
||||
const y = viewport ? Math.max(viewport.height - 5, 0) : 800
|
||||
await page.mouse.move(x, y)
|
||||
return menu
|
||||
throw new Error(`Failed to open project menu: ${projectSlug}`)
|
||||
}
|
||||
|
||||
export async function setWorkspacesEnabled(page: Page, projectSlug: string, enabled: boolean) {
|
||||
@@ -556,12 +684,18 @@ export async function setWorkspacesEnabled(page: Page, projectSlug: string, enab
|
||||
|
||||
if (current === enabled) return
|
||||
|
||||
await openProjectMenu(page, projectSlug)
|
||||
const flip = async (timeout?: number) => {
|
||||
const menu = await openProjectMenu(page, projectSlug)
|
||||
const toggle = menu.locator(projectWorkspacesToggleSelector(projectSlug)).first()
|
||||
await expect(toggle).toBeVisible()
|
||||
return toggle.click({ force: true, timeout })
|
||||
}
|
||||
|
||||
const toggle = page.locator(projectWorkspacesToggleSelector(projectSlug)).first()
|
||||
await expect(toggle).toBeVisible()
|
||||
await page.waitForTimeout(100) // kilocode_change - wait before toggle click
|
||||
await toggle.click({ force: true })
|
||||
const flipped = await flip(1500)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (!flipped) await flip()
|
||||
|
||||
const expected = enabled ? "New workspace" : "New session"
|
||||
await expect(page.getByRole("button", { name: expected }).first()).toBeVisible()
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { serverName } from "../utils"
|
||||
import { serverNamePattern } from "../utils"
|
||||
|
||||
test("home renders and shows core entrypoints", async ({ page }) => {
|
||||
await page.goto("/")
|
||||
|
||||
await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible()
|
||||
await expect(page.getByRole("button", { name: serverName })).toBeVisible()
|
||||
await expect(page.getByRole("button", { name: serverNamePattern })).toBeVisible()
|
||||
})
|
||||
|
||||
test("server picker dialog opens from home", async ({ page }) => {
|
||||
await page.goto("/")
|
||||
|
||||
const trigger = page.getByRole("button", { name: serverName })
|
||||
const trigger = page.getByRole("button", { name: serverNamePattern })
|
||||
await expect(trigger).toBeVisible()
|
||||
await trigger.click()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { serverName, serverUrl } from "../utils"
|
||||
import { clickListItem, closeDialog, clickMenuItem } from "../actions"
|
||||
import { serverNamePattern, serverUrls } from "../utils"
|
||||
import { closeDialog, clickMenuItem } from "../actions"
|
||||
|
||||
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
|
||||
|
||||
@@ -31,10 +31,9 @@ test("can set a default server on web", async ({ page, gotoSession }) => {
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
const row = dialog.locator('[data-slot="list-item"]').filter({ hasText: serverName }).first()
|
||||
await expect(row).toBeVisible()
|
||||
await expect(dialog.getByText(serverNamePattern).first()).toBeVisible()
|
||||
|
||||
const menuTrigger = row.locator('[data-slot="dropdown-menu-trigger"]').first()
|
||||
const menuTrigger = dialog.locator('[data-slot="dropdown-menu-trigger"]').first()
|
||||
await expect(menuTrigger).toBeVisible()
|
||||
await menuTrigger.click({ force: true })
|
||||
|
||||
@@ -42,14 +41,18 @@ test("can set a default server on web", async ({ page, gotoSession }) => {
|
||||
await expect(menu).toBeVisible()
|
||||
await clickMenuItem(menu, /set as default/i)
|
||||
|
||||
await expect.poll(() => page.evaluate((key) => localStorage.getItem(key), DEFAULT_SERVER_URL_KEY)).toBe(serverUrl)
|
||||
await expect(row.getByText("Default", { exact: true })).toBeVisible()
|
||||
await expect
|
||||
.poll(async () =>
|
||||
serverUrls.includes((await page.evaluate((key) => localStorage.getItem(key), DEFAULT_SERVER_URL_KEY)) ?? ""),
|
||||
)
|
||||
.toBe(true)
|
||||
await expect(dialog.getByText("Default", { exact: true })).toBeVisible()
|
||||
|
||||
await closeDialog(page, dialog)
|
||||
|
||||
await ensurePopoverOpen()
|
||||
|
||||
const serverRow = popover.locator("button").filter({ hasText: serverName }).first()
|
||||
const serverRow = popover.locator("button").filter({ hasText: serverNamePattern }).first()
|
||||
await expect(serverRow).toBeVisible()
|
||||
await expect(serverRow.getByText("Default", { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -16,7 +16,6 @@ test("titlebar back/forward navigates between sessions", async ({ page, slug, sd
|
||||
|
||||
const link = page.locator(`[data-session-id="${two.id}"] a`).first()
|
||||
await expect(link).toBeVisible()
|
||||
await link.scrollIntoViewIfNeeded()
|
||||
await link.click()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
|
||||
@@ -56,7 +55,6 @@ test("titlebar forward is cleared after branching history from sidebar", async (
|
||||
|
||||
const second = page.locator(`[data-session-id="${b.id}"] a`).first()
|
||||
await expect(second).toBeVisible()
|
||||
await second.scrollIntoViewIfNeeded()
|
||||
await second.click()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${b.id}(?:\\?|#|$)`))
|
||||
@@ -76,7 +74,6 @@ test("titlebar forward is cleared after branching history from sidebar", async (
|
||||
|
||||
const third = page.locator(`[data-session-id="${c.id}"] a`).first()
|
||||
await expect(third).toBeVisible()
|
||||
await third.scrollIntoViewIfNeeded()
|
||||
await third.click()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${c.id}(?:\\?|#|$)`))
|
||||
@@ -102,7 +99,6 @@ test("keyboard shortcuts navigate titlebar history", async ({ page, slug, sdk, g
|
||||
|
||||
const link = page.locator(`[data-session-id="${two.id}"] a`).first()
|
||||
await expect(link).toBeVisible()
|
||||
await link.scrollIntoViewIfNeeded()
|
||||
await link.click()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
|
||||
|
||||
@@ -10,6 +10,8 @@ const expanded = async (el: { getAttribute: (name: string) => Promise<string | n
|
||||
test("review panel can be toggled via keybind", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const reviewPanel = page.locator("#review-panel")
|
||||
|
||||
const treeToggle = page.getByRole("button", { name: "Toggle file tree" }).first()
|
||||
await expect(treeToggle).toBeVisible()
|
||||
if (await expanded(treeToggle)) await treeToggle.click()
|
||||
@@ -19,13 +21,13 @@ test("review panel can be toggled via keybind", async ({ page, gotoSession }) =>
|
||||
await expect(reviewToggle).toBeVisible()
|
||||
if (await expanded(reviewToggle)) await reviewToggle.click()
|
||||
await expect(reviewToggle).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(page.locator("#review-panel")).toHaveCount(0)
|
||||
await expect(reviewPanel).toHaveAttribute("aria-hidden", "true")
|
||||
|
||||
await page.keyboard.press(`${modKey}+Shift+R`)
|
||||
await expect(reviewToggle).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(page.locator("#review-panel")).toBeVisible()
|
||||
await expect(reviewPanel).toHaveAttribute("aria-hidden", "false")
|
||||
|
||||
await page.keyboard.press(`${modKey}+Shift+R`)
|
||||
await expect(reviewToggle).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(page.locator("#review-panel")).toHaveCount(0)
|
||||
await expect(reviewPanel).toHaveAttribute("aria-hidden", "true")
|
||||
})
|
||||
|
||||
@@ -43,6 +43,13 @@ test("file tree can expand folders and open a file", async ({ page, gotoSession
|
||||
await tab.click()
|
||||
await expect(tab).toHaveAttribute("aria-selected", "true")
|
||||
|
||||
await toggle.click()
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "false")
|
||||
|
||||
await toggle.click()
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(allTab).toHaveAttribute("aria-selected", "true")
|
||||
|
||||
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
|
||||
await expect(viewer).toBeVisible()
|
||||
await expect(viewer).toContainText("export default function FileTree")
|
||||
|
||||
@@ -101,3 +101,56 @@ test("cmd+f opens text viewer search while prompt is focused", async ({ page, go
|
||||
await expect(findInput).toBeVisible()
|
||||
await expect(findInput).toBeFocused()
|
||||
})
|
||||
|
||||
test("cmd+f opens text viewer search while prompt is not focused", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
await page.keyboard.type("/open")
|
||||
|
||||
const command = page.locator('[data-slash-id="file.open"]').first()
|
||||
await expect(command).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
const dialog = page
|
||||
.getByRole("dialog")
|
||||
.filter({ has: page.getByPlaceholder(/search files/i) })
|
||||
.first()
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
const input = dialog.getByRole("textbox").first()
|
||||
await input.fill("package.json")
|
||||
|
||||
const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]')
|
||||
let index = -1
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? ""))
|
||||
index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, "")))
|
||||
return index >= 0
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
const item = items.nth(index)
|
||||
await expect(item).toBeVisible()
|
||||
await item.click()
|
||||
|
||||
await expect(dialog).toHaveCount(0)
|
||||
|
||||
const tab = page.getByRole("tab", { name: "package.json" })
|
||||
await expect(tab).toBeVisible()
|
||||
await tab.click()
|
||||
|
||||
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
|
||||
await expect(viewer).toBeVisible()
|
||||
|
||||
await viewer.click()
|
||||
await page.keyboard.press(`${modKey}+f`)
|
||||
|
||||
const findInput = page.getByPlaceholder("Find")
|
||||
await expect(findInput).toBeVisible()
|
||||
await expect(findInput).toBeFocused()
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test as base, expect, type Page } from "@playwright/test"
|
||||
import { cleanupTestProject, createTestProject, seedProjects } from "./actions"
|
||||
import { cleanupSession, cleanupTestProject, createTestProject, seedProjects, sessionIDFromUrl } from "./actions"
|
||||
import { promptSelector } from "./selectors"
|
||||
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
||||
|
||||
@@ -13,6 +13,8 @@ type TestFixtures = {
|
||||
directory: string
|
||||
slug: string
|
||||
gotoSession: (sessionID?: string) => Promise<void>
|
||||
trackSession: (sessionID: string, directory?: string) => void
|
||||
trackDirectory: (directory: string) => void
|
||||
}) => Promise<T>,
|
||||
options?: { extra?: string[] },
|
||||
) => Promise<T>
|
||||
@@ -51,20 +53,36 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
||||
},
|
||||
withProject: async ({ page }, use) => {
|
||||
await use(async (callback, options) => {
|
||||
const directory = await createTestProject()
|
||||
const slug = dirSlug(directory)
|
||||
await seedStorage(page, { directory, extra: options?.extra })
|
||||
const root = await createTestProject()
|
||||
const slug = dirSlug(root)
|
||||
const sessions = new Map<string, string>()
|
||||
const dirs = new Set<string>()
|
||||
await seedStorage(page, { directory: root, extra: options?.extra })
|
||||
|
||||
const gotoSession = async (sessionID?: string) => {
|
||||
await page.goto(sessionPath(directory, sessionID))
|
||||
await page.goto(sessionPath(root, sessionID))
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
const current = sessionIDFromUrl(page.url())
|
||||
if (current) trackSession(current)
|
||||
}
|
||||
|
||||
const trackSession = (sessionID: string, directory?: string) => {
|
||||
sessions.set(sessionID, directory ?? root)
|
||||
}
|
||||
|
||||
const trackDirectory = (directory: string) => {
|
||||
if (directory !== root) dirs.add(directory)
|
||||
}
|
||||
|
||||
try {
|
||||
await gotoSession()
|
||||
return await callback({ directory, slug, gotoSession })
|
||||
return await callback({ directory: root, slug, gotoSession, trackSession, trackDirectory })
|
||||
} finally {
|
||||
await cleanupTestProject(directory)
|
||||
await Promise.allSettled(
|
||||
Array.from(sessions, ([sessionID, directory]) => cleanupSession({ sessionID, directory })),
|
||||
)
|
||||
await Promise.allSettled(Array.from(dirs, (directory) => cleanupTestProject(directory)))
|
||||
await cleanupTestProject(root)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -1,29 +1,16 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { openSidebar } from "../actions"
|
||||
import { clickMenuItem, openProjectMenu, openSidebar } from "../actions"
|
||||
|
||||
test("dialog edit project updates name and startup script", async ({ page, withProject }) => {
|
||||
test.skip(process.platform === "win32", "Skipping on Windows due to hover/menu interaction issues") // kilocode_change
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
await withProject(async () => {
|
||||
await withProject(async ({ slug }) => {
|
||||
await openSidebar(page)
|
||||
|
||||
const open = async () => {
|
||||
const header = page.locator(".group\\/project").first()
|
||||
await header.hover()
|
||||
await page.waitForTimeout(200) // kilocode_change - wait for hover state
|
||||
const trigger = header.getByRole("button", { name: "More options" }).first()
|
||||
await expect(trigger).toBeVisible()
|
||||
await page.waitForTimeout(100) // kilocode_change - wait before trigger click
|
||||
await trigger.click({ force: true })
|
||||
|
||||
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
|
||||
await expect(menu).toBeVisible()
|
||||
|
||||
const editItem = menu.getByRole("menuitem", { name: "Edit" }).first()
|
||||
await expect(editItem).toBeVisible()
|
||||
await page.waitForTimeout(100) // kilocode_change - wait before edit item click
|
||||
await editItem.click({ force: true })
|
||||
const menu = await openProjectMenu(page, slug)
|
||||
await clickMenuItem(menu, /^Edit$/i, { force: true })
|
||||
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
@@ -1,36 +1,8 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { createTestProject, cleanupTestProject, openSidebar, clickMenuItem, openProjectMenu } from "../actions"
|
||||
import { projectCloseHoverSelector, projectSwitchSelector } from "../selectors"
|
||||
import { projectSwitchSelector } from "../selectors"
|
||||
import { dirSlug } from "../utils"
|
||||
|
||||
test("can close a project via hover card close button", async ({ page, withProject }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
const other = await createTestProject()
|
||||
const otherSlug = dirSlug(other)
|
||||
|
||||
try {
|
||||
await withProject(
|
||||
async () => {
|
||||
await openSidebar(page)
|
||||
|
||||
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
||||
await expect(otherButton).toBeVisible()
|
||||
await otherButton.hover()
|
||||
|
||||
const close = page.locator(projectCloseHoverSelector(otherSlug)).first()
|
||||
await expect(close).toBeVisible()
|
||||
await close.click()
|
||||
|
||||
await expect(otherButton).toHaveCount(0)
|
||||
},
|
||||
{ extra: [other] },
|
||||
)
|
||||
} finally {
|
||||
await cleanupTestProject(other)
|
||||
}
|
||||
})
|
||||
|
||||
test("closing active project navigates to another open project", async ({ page, withProject }) => {
|
||||
test.skip(process.platform === "win32", "Skipping on Windows due to workspace interaction issues") // kilocode_change
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
@@ -54,16 +26,26 @@ test("closing active project navigates to another open project", async ({ page,
|
||||
await clickMenuItem(menu, /^Close$/i, { force: true })
|
||||
|
||||
await expect
|
||||
.poll(() => {
|
||||
const pathname = new URL(page.url()).pathname
|
||||
if (new RegExp(`^/${slug}/session(?:/[^/]+)?/?$`).test(pathname)) return "project"
|
||||
if (pathname === "/") return "home"
|
||||
return ""
|
||||
})
|
||||
.poll(
|
||||
() => {
|
||||
const pathname = new URL(page.url()).pathname
|
||||
if (new RegExp(`^/${slug}/session(?:/[^/]+)?/?$`).test(pathname)) return "project"
|
||||
if (pathname === "/") return "home"
|
||||
return ""
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
.toMatch(/^(project|home)$/)
|
||||
|
||||
await expect(page).not.toHaveURL(new RegExp(`/${otherSlug}/session(?:[/?#]|$)`))
|
||||
await expect(otherButton).toHaveCount(0)
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
return await page.locator(projectSwitchSelector(otherSlug)).count()
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
.toBe(0)
|
||||
},
|
||||
{ extra: [other] },
|
||||
)
|
||||
|
||||
@@ -1,18 +1,39 @@
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import type { Page } from "@playwright/test"
|
||||
import { test, expect } from "../fixtures"
|
||||
import {
|
||||
defocus,
|
||||
createTestProject,
|
||||
cleanupTestProject,
|
||||
openSidebar,
|
||||
setWorkspacesEnabled,
|
||||
sessionIDFromUrl,
|
||||
} from "../actions"
|
||||
import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl, waitSlug } from "../actions"
|
||||
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
||||
import { createSdk, dirSlug, sessionPath } from "../utils"
|
||||
import { dirSlug, resolveDirectory } from "../utils"
|
||||
|
||||
function slugFromUrl(url: string) {
|
||||
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
||||
async function workspaces(page: Page, directory: string, enabled: boolean) {
|
||||
await page.evaluate(
|
||||
({ directory, enabled }: { directory: string; enabled: boolean }) => {
|
||||
const key = "opencode.global.dat:layout"
|
||||
const raw = localStorage.getItem(key)
|
||||
const data = raw ? JSON.parse(raw) : {}
|
||||
const sidebar = data.sidebar && typeof data.sidebar === "object" ? data.sidebar : {}
|
||||
const current =
|
||||
sidebar.workspaces && typeof sidebar.workspaces === "object" && !Array.isArray(sidebar.workspaces)
|
||||
? sidebar.workspaces
|
||||
: {}
|
||||
const next = { ...current }
|
||||
|
||||
if (enabled) next[directory] = true
|
||||
if (!enabled) delete next[directory]
|
||||
|
||||
localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
...data,
|
||||
sidebar: {
|
||||
...sidebar,
|
||||
workspaces: next,
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
{ directory, enabled },
|
||||
)
|
||||
}
|
||||
|
||||
test("can switch between projects from sidebar", async ({ page, withProject }) => {
|
||||
@@ -52,46 +73,39 @@ test("switching back to a project opens the latest workspace session", async ({
|
||||
|
||||
const other = await createTestProject()
|
||||
const otherSlug = dirSlug(other)
|
||||
let rootDir: string | undefined
|
||||
let workspaceDir: string | undefined
|
||||
let sessionID: string | undefined
|
||||
|
||||
try {
|
||||
await withProject(
|
||||
async ({ directory, slug }) => {
|
||||
rootDir = directory
|
||||
async ({ directory, slug, trackSession, trackDirectory }) => {
|
||||
await defocus(page)
|
||||
await workspaces(page, directory, true)
|
||||
await page.reload()
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
await openSidebar(page)
|
||||
await setWorkspacesEnabled(page, slug, true)
|
||||
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
||||
|
||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
const next = slugFromUrl(page.url())
|
||||
if (!next) return ""
|
||||
if (next === slug) return ""
|
||||
return next
|
||||
},
|
||||
{ timeout: 45_000 },
|
||||
)
|
||||
.not.toBe("")
|
||||
|
||||
const workspaceSlug = slugFromUrl(page.url())
|
||||
workspaceDir = base64Decode(workspaceSlug)
|
||||
if (!workspaceDir) throw new Error(`Failed to decode workspace slug: ${workspaceSlug}`)
|
||||
const raw = await waitSlug(page, [slug])
|
||||
const dir = base64Decode(raw)
|
||||
if (!dir) throw new Error(`Failed to decode workspace slug: ${raw}`)
|
||||
const space = await resolveDirectory(dir)
|
||||
const next = dirSlug(space)
|
||||
trackDirectory(space)
|
||||
await openSidebar(page)
|
||||
|
||||
const workspace = page.locator(workspaceItemSelector(workspaceSlug)).first()
|
||||
await expect(workspace).toBeVisible()
|
||||
await workspace.hover()
|
||||
const item = page.locator(`${workspaceItemSelector(next)}, ${workspaceItemSelector(raw)}`).first()
|
||||
await expect(item).toBeVisible()
|
||||
await item.hover()
|
||||
|
||||
const newSession = page.locator(workspaceNewSessionSelector(workspaceSlug)).first()
|
||||
await expect(newSession).toBeVisible()
|
||||
await newSession.click({ force: true })
|
||||
const btn = page.locator(`${workspaceNewSessionSelector(next)}, ${workspaceNewSessionSelector(raw)}`).first()
|
||||
await expect(btn).toBeVisible()
|
||||
await btn.click({ force: true })
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session(?:[/?#]|$)`))
|
||||
// A new workspace can be discovered via a transient slug before the route and sidebar
|
||||
// settle to the canonical workspace path on Windows, so interact with either and assert
|
||||
// against the resolved workspace slug.
|
||||
await waitSlug(page)
|
||||
await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
|
||||
|
||||
// Create a session by sending a prompt
|
||||
const prompt = page.locator(promptSelector)
|
||||
@@ -104,9 +118,9 @@ test("switching back to a project opens the latest workspace session", async ({
|
||||
|
||||
const created = sessionIDFromUrl(page.url())
|
||||
if (!created) throw new Error(`Failed to get session ID from url: ${page.url()}`)
|
||||
sessionID = created
|
||||
trackSession(created, space)
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`))
|
||||
await expect(page).toHaveURL(new RegExp(`/${next}/session/${created}(?:[/?#]|$)`))
|
||||
|
||||
await openSidebar(page)
|
||||
|
||||
@@ -125,20 +139,6 @@ test("switching back to a project opens the latest workspace session", async ({
|
||||
{ extra: [other] },
|
||||
)
|
||||
} finally {
|
||||
if (sessionID) {
|
||||
const id = sessionID
|
||||
const dirs = [rootDir, workspaceDir].filter((x): x is string => !!x)
|
||||
await Promise.all(
|
||||
dirs.map((directory) =>
|
||||
createSdk(directory)
|
||||
.session.delete({ sessionID: id })
|
||||
.catch(() => undefined),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (workspaceDir) {
|
||||
await cleanupTestProject(workspaceDir)
|
||||
}
|
||||
await cleanupTestProject(other)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import type { Page } from "@playwright/test"
|
||||
import { test, expect } from "../fixtures"
|
||||
import { cleanupTestProject, openSidebar, sessionIDFromUrl, setWorkspacesEnabled } from "../actions"
|
||||
import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, slugFromUrl, waitSlug } from "../actions"
|
||||
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
||||
import { createSdk } from "../utils"
|
||||
|
||||
function slugFromUrl(url: string) {
|
||||
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
||||
}
|
||||
|
||||
async function waitWorkspaceReady(page: Page, slug: string) {
|
||||
await openSidebar(page)
|
||||
await expect
|
||||
@@ -31,20 +27,7 @@ async function createWorkspace(page: Page, root: string, seen: string[]) {
|
||||
await openSidebar(page)
|
||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
const slug = slugFromUrl(page.url())
|
||||
if (!slug) return ""
|
||||
if (slug === root) return ""
|
||||
if (seen.includes(slug)) return ""
|
||||
return slug
|
||||
},
|
||||
{ timeout: 45_000 },
|
||||
)
|
||||
.not.toBe("")
|
||||
|
||||
const slug = slugFromUrl(page.url())
|
||||
const slug = await waitSlug(page, [root, ...seen])
|
||||
const directory = base64Decode(slug)
|
||||
if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
|
||||
return { slug, directory }
|
||||
@@ -60,12 +43,13 @@ async function openWorkspaceNewSession(page: Page, slug: string) {
|
||||
await expect(button).toBeVisible()
|
||||
await button.click({ force: true })
|
||||
|
||||
await expect.poll(() => slugFromUrl(page.url())).toBe(slug)
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session(?:[/?#]|$)`))
|
||||
const next = await waitSlug(page)
|
||||
await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
|
||||
return next
|
||||
}
|
||||
|
||||
async function createSessionFromWorkspace(page: Page, slug: string, text: string) {
|
||||
await openWorkspaceNewSession(page, slug)
|
||||
const next = await openWorkspaceNewSession(page, slug)
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
await expect(prompt).toBeVisible()
|
||||
@@ -76,13 +60,13 @@ async function createSessionFromWorkspace(page: Page, slug: string, text: string
|
||||
await expect.poll(async () => ((await prompt.textContent()) ?? "").trim()).toContain(text)
|
||||
await prompt.press("Enter")
|
||||
|
||||
await expect.poll(() => slugFromUrl(page.url())).toBe(slug)
|
||||
await expect.poll(() => slugFromUrl(page.url())).toBe(next)
|
||||
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("")
|
||||
|
||||
const sessionID = sessionIDFromUrl(page.url())
|
||||
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${sessionID}(?:[/?#]|$)`))
|
||||
return sessionID
|
||||
await expect(page).toHaveURL(new RegExp(`/${next}/session/${sessionID}(?:[/?#]|$)`))
|
||||
return { sessionID, slug: next }
|
||||
}
|
||||
|
||||
async function sessionDirectory(directory: string, sessionID: string) {
|
||||
@@ -98,48 +82,29 @@ test("new sessions from sidebar workspace actions stay in selected workspace", a
|
||||
test.skip(process.platform === "win32", "Skipping on Windows due to workspace interaction issues") // kilocode_change
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
await withProject(async ({ directory, slug: root }) => {
|
||||
const workspaces = [] as { slug: string; directory: string }[]
|
||||
const sessions = [] as string[]
|
||||
await withProject(async ({ directory, slug: root, trackSession, trackDirectory }) => {
|
||||
await openSidebar(page)
|
||||
await setWorkspacesEnabled(page, root, true)
|
||||
|
||||
try {
|
||||
await openSidebar(page)
|
||||
await setWorkspacesEnabled(page, root, true)
|
||||
const first = await createWorkspace(page, root, [])
|
||||
trackDirectory(first.directory)
|
||||
await waitWorkspaceReady(page, first.slug)
|
||||
|
||||
const first = await createWorkspace(page, root, [])
|
||||
workspaces.push(first)
|
||||
await waitWorkspaceReady(page, first.slug)
|
||||
const second = await createWorkspace(page, root, [first.slug])
|
||||
trackDirectory(second.directory)
|
||||
await waitWorkspaceReady(page, second.slug)
|
||||
|
||||
const second = await createWorkspace(page, root, [first.slug])
|
||||
workspaces.push(second)
|
||||
await waitWorkspaceReady(page, second.slug)
|
||||
const firstSession = await createSessionFromWorkspace(page, first.slug, `workspace one ${Date.now()}`)
|
||||
trackSession(firstSession.sessionID, first.directory)
|
||||
|
||||
const firstSession = await createSessionFromWorkspace(page, first.slug, `workspace one ${Date.now()}`)
|
||||
sessions.push(firstSession)
|
||||
const secondSession = await createSessionFromWorkspace(page, second.slug, `workspace two ${Date.now()}`)
|
||||
trackSession(secondSession.sessionID, second.directory)
|
||||
|
||||
const secondSession = await createSessionFromWorkspace(page, second.slug, `workspace two ${Date.now()}`)
|
||||
sessions.push(secondSession)
|
||||
const thirdSession = await createSessionFromWorkspace(page, first.slug, `workspace one again ${Date.now()}`)
|
||||
trackSession(thirdSession.sessionID, first.directory)
|
||||
|
||||
const thirdSession = await createSessionFromWorkspace(page, first.slug, `workspace one again ${Date.now()}`)
|
||||
sessions.push(thirdSession)
|
||||
|
||||
await expect.poll(() => sessionDirectory(first.directory, firstSession)).toBe(first.directory)
|
||||
await expect.poll(() => sessionDirectory(second.directory, secondSession)).toBe(second.directory)
|
||||
await expect.poll(() => sessionDirectory(first.directory, thirdSession)).toBe(first.directory)
|
||||
} finally {
|
||||
const dirs = [directory, ...workspaces.map((workspace) => workspace.directory)]
|
||||
await Promise.all(
|
||||
sessions.map((sessionID) =>
|
||||
Promise.all(
|
||||
dirs.map((dir) =>
|
||||
createSdk(dir)
|
||||
.session.delete({ sessionID })
|
||||
.catch(() => undefined),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
await Promise.all(workspaces.map((workspace) => cleanupTestProject(workspace.directory)))
|
||||
}
|
||||
await expect.poll(() => sessionDirectory(first.directory, firstSession.sessionID)).toBe(first.directory)
|
||||
await expect.poll(() => sessionDirectory(second.directory, secondSession.sessionID)).toBe(second.directory)
|
||||
await expect.poll(() => sessionDirectory(first.directory, thirdSession.sessionID)).toBe(first.directory)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,14 +14,12 @@ import {
|
||||
openSidebar,
|
||||
openWorkspaceMenu,
|
||||
setWorkspacesEnabled,
|
||||
slugFromUrl,
|
||||
waitSlug,
|
||||
} from "../actions"
|
||||
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
|
||||
import { createSdk, dirSlug } from "../utils"
|
||||
|
||||
function slugFromUrl(url: string) {
|
||||
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
||||
}
|
||||
|
||||
async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
||||
const rootSlug = project.slug
|
||||
await openSidebar(page)
|
||||
@@ -29,17 +27,7 @@ async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
||||
await setWorkspacesEnabled(page, rootSlug, true)
|
||||
|
||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
const slug = slugFromUrl(page.url())
|
||||
return slug.length > 0 && slug !== rootSlug
|
||||
},
|
||||
{ timeout: 45_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
const slug = slugFromUrl(page.url())
|
||||
const slug = await waitSlug(page, [rootSlug])
|
||||
const dir = base64Decode(slug)
|
||||
|
||||
await openSidebar(page)
|
||||
@@ -95,18 +83,7 @@ test.skip("can create a workspace", async ({ page, withProject }) => {
|
||||
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
||||
|
||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
const currentSlug = slugFromUrl(page.url())
|
||||
return currentSlug.length > 0 && currentSlug !== slug
|
||||
},
|
||||
{ timeout: 45_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
const workspaceSlug = slugFromUrl(page.url())
|
||||
const workspaceSlug = await waitSlug(page, [slug])
|
||||
const workspaceDir = base64Decode(workspaceSlug)
|
||||
|
||||
await openSidebar(page)
|
||||
@@ -290,7 +267,7 @@ test.skip("can delete a workspace", async ({ page, withProject }) => {
|
||||
await clickMenuItem(menu, /^Delete$/i, { force: true })
|
||||
await confirmDialog(page, /^Delete workspace$/i)
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
|
||||
await expect.poll(() => base64Decode(slugFromUrl(page.url()))).toBe(project.directory)
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
@@ -349,9 +326,6 @@ test.skip("can reorder workspaces by drag and drop", async ({ page, withProject
|
||||
const src = page.locator(workspaceItemSelector(from)).first()
|
||||
const dst = page.locator(workspaceItemSelector(to)).first()
|
||||
|
||||
await src.scrollIntoViewIfNeeded()
|
||||
await dst.scrollIntoViewIfNeeded()
|
||||
|
||||
const a = await src.boundingBox()
|
||||
const b = await dst.boundingBox()
|
||||
if (!a || !b) throw new Error("Failed to resolve workspace drag bounds")
|
||||
@@ -370,17 +344,7 @@ test.skip("can reorder workspaces by drag and drop", async ({ page, withProject
|
||||
for (const _ of [0, 1]) {
|
||||
const prev = slugFromUrl(page.url())
|
||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
const slug = slugFromUrl(page.url())
|
||||
return slug.length > 0 && slug !== rootSlug && slug !== prev
|
||||
},
|
||||
{ timeout: 45_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
const slug = slugFromUrl(page.url())
|
||||
const slug = await waitSlug(page, [rootSlug, prev])
|
||||
const dir = base64Decode(slug)
|
||||
workspaces.push({ slug, directory: dir })
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector } from "../selectors"
|
||||
import { sessionIDFromUrl } from "../actions"
|
||||
import { cleanupSession, sessionIDFromUrl, withSession } from "../actions"
|
||||
|
||||
const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
|
||||
|
||||
// Regression test for Issue #12453: the synchronous POST /message endpoint holds
|
||||
// the connection open while the agent works, causing "Failed to fetch" over
|
||||
@@ -38,6 +40,37 @@ test("prompt succeeds when sync message endpoint is unreachable", async ({ page,
|
||||
)
|
||||
.toContain(token)
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID }).catch(() => undefined)
|
||||
await cleanupSession({ sdk, sessionID })
|
||||
}
|
||||
})
|
||||
|
||||
test("failed prompt send restores the composer input", async ({ page, sdk, gotoSession }) => {
|
||||
await withSession(sdk, `e2e prompt failure ${Date.now()}`, async (session) => {
|
||||
const prompt = page.locator(promptSelector)
|
||||
const value = `restore ${Date.now()}`
|
||||
|
||||
await page.route(`**/session/${session.id}/prompt_async`, (route) =>
|
||||
route.fulfill({
|
||||
status: 500,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ message: "e2e prompt failure" }),
|
||||
}),
|
||||
)
|
||||
|
||||
await gotoSession(session.id)
|
||||
await prompt.click()
|
||||
await page.keyboard.type(value)
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect.poll(async () => text(await prompt.textContent())).toBe(value)
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const messages = await sdk.session.messages({ sessionID: session.id, limit: 50 }).then((r) => r.data ?? [])
|
||||
return messages.length
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
.toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import type { ToolPart } from "@kilocode/sdk/v2/client"
|
||||
import type { Page } from "@playwright/test"
|
||||
import { test, expect } from "../fixtures"
|
||||
import { withSession } from "../actions"
|
||||
import { promptSelector } from "../selectors"
|
||||
|
||||
const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
|
||||
|
||||
const isBash = (part: unknown): part is ToolPart => {
|
||||
if (!part || typeof part !== "object") return false
|
||||
if (!("type" in part) || part.type !== "tool") return false
|
||||
if (!("tool" in part) || part.tool !== "bash") return false
|
||||
return "state" in part
|
||||
}
|
||||
|
||||
async function edge(page: Page, pos: "start" | "end") {
|
||||
await page.locator(promptSelector).evaluate((el: HTMLDivElement, pos: "start" | "end") => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection) return
|
||||
|
||||
const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
|
||||
const nodes: Text[] = []
|
||||
for (let node = walk.nextNode(); node; node = walk.nextNode()) {
|
||||
nodes.push(node as Text)
|
||||
}
|
||||
|
||||
if (nodes.length === 0) {
|
||||
const node = document.createTextNode("")
|
||||
el.appendChild(node)
|
||||
nodes.push(node)
|
||||
}
|
||||
|
||||
const node = pos === "start" ? nodes[0]! : nodes[nodes.length - 1]!
|
||||
const range = document.createRange()
|
||||
range.setStart(node, pos === "start" ? 0 : (node.textContent ?? "").length)
|
||||
range.collapse(true)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}, pos)
|
||||
}
|
||||
|
||||
async function wait(page: Page, value: string) {
|
||||
await expect.poll(async () => text(await page.locator(promptSelector).textContent())).toBe(value)
|
||||
}
|
||||
|
||||
async function reply(sdk: Parameters<typeof withSession>[0], sessionID: string, token: string) {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
|
||||
return messages
|
||||
.filter((item) => item.info.role === "assistant")
|
||||
.flatMap((item) => item.parts)
|
||||
.filter((item) => item.type === "text")
|
||||
.map((item) => item.text)
|
||||
.join("\n")
|
||||
},
|
||||
{ timeout: 90_000 },
|
||||
)
|
||||
.toContain(token)
|
||||
}
|
||||
|
||||
async function shell(sdk: Parameters<typeof withSession>[0], sessionID: string, cmd: string, token: string) {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
|
||||
const part = messages
|
||||
.filter((item) => item.info.role === "assistant")
|
||||
.flatMap((item) => item.parts)
|
||||
.filter(isBash)
|
||||
.find((item) => item.state.input?.command === cmd && item.state.status === "completed")
|
||||
|
||||
if (!part || part.state.status !== "completed") return
|
||||
return typeof part.state.metadata?.output === "string" ? part.state.metadata.output : part.state.output
|
||||
},
|
||||
{ timeout: 90_000 },
|
||||
)
|
||||
.toContain(token)
|
||||
}
|
||||
|
||||
test("prompt history restores unsent draft with arrow navigation", async ({ page, sdk, gotoSession }) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
await withSession(sdk, `e2e prompt history ${Date.now()}`, async (session) => {
|
||||
await gotoSession(session.id)
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
const firstToken = `E2E_HISTORY_ONE_${Date.now()}`
|
||||
const secondToken = `E2E_HISTORY_TWO_${Date.now()}`
|
||||
const first = `Reply with exactly: ${firstToken}`
|
||||
const second = `Reply with exactly: ${secondToken}`
|
||||
const draft = `draft ${Date.now()}`
|
||||
|
||||
await prompt.click()
|
||||
await page.keyboard.type(first)
|
||||
await page.keyboard.press("Enter")
|
||||
await wait(page, "")
|
||||
await reply(sdk, session.id, firstToken)
|
||||
|
||||
await prompt.click()
|
||||
await page.keyboard.type(second)
|
||||
await page.keyboard.press("Enter")
|
||||
await wait(page, "")
|
||||
await reply(sdk, session.id, secondToken)
|
||||
|
||||
await prompt.click()
|
||||
await page.keyboard.type(draft)
|
||||
await wait(page, draft)
|
||||
|
||||
await edge(page, "start")
|
||||
await page.keyboard.press("ArrowUp")
|
||||
await wait(page, second)
|
||||
|
||||
await page.keyboard.press("ArrowUp")
|
||||
await wait(page, first)
|
||||
|
||||
await page.keyboard.press("ArrowDown")
|
||||
await wait(page, second)
|
||||
|
||||
await page.keyboard.press("ArrowDown")
|
||||
await wait(page, draft)
|
||||
})
|
||||
})
|
||||
|
||||
test("shell history stays separate from normal prompt history", async ({ page, sdk, gotoSession }) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
await withSession(sdk, `e2e shell history ${Date.now()}`, async (session) => {
|
||||
await gotoSession(session.id)
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
const firstToken = `E2E_SHELL_ONE_${Date.now()}`
|
||||
const secondToken = `E2E_SHELL_TWO_${Date.now()}`
|
||||
const normalToken = `E2E_NORMAL_${Date.now()}`
|
||||
const first = `echo ${firstToken}`
|
||||
const second = `echo ${secondToken}`
|
||||
const normal = `Reply with exactly: ${normalToken}`
|
||||
|
||||
await prompt.click()
|
||||
await page.keyboard.type("!")
|
||||
await page.keyboard.type(first)
|
||||
await page.keyboard.press("Enter")
|
||||
await wait(page, "")
|
||||
await shell(sdk, session.id, first, firstToken)
|
||||
|
||||
await prompt.click()
|
||||
await page.keyboard.type("!")
|
||||
await page.keyboard.type(second)
|
||||
await page.keyboard.press("Enter")
|
||||
await wait(page, "")
|
||||
await shell(sdk, session.id, second, secondToken)
|
||||
|
||||
await prompt.click()
|
||||
await page.keyboard.type("!")
|
||||
await page.keyboard.press("ArrowUp")
|
||||
await wait(page, second)
|
||||
|
||||
await page.keyboard.press("ArrowUp")
|
||||
await wait(page, first)
|
||||
|
||||
await page.keyboard.press("ArrowDown")
|
||||
await wait(page, second)
|
||||
|
||||
await page.keyboard.press("ArrowDown")
|
||||
await wait(page, "")
|
||||
|
||||
await page.keyboard.press("Escape")
|
||||
await wait(page, "")
|
||||
|
||||
await prompt.click()
|
||||
await page.keyboard.type(normal)
|
||||
await page.keyboard.press("Enter")
|
||||
await wait(page, "")
|
||||
await reply(sdk, session.id, normalToken)
|
||||
|
||||
await prompt.click()
|
||||
await page.keyboard.press("ArrowUp")
|
||||
await wait(page, normal)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { ToolPart } from "@kilocode/sdk/v2/client"
|
||||
import { test, expect } from "../fixtures"
|
||||
import { sessionIDFromUrl } from "../actions"
|
||||
import { promptSelector } from "../selectors"
|
||||
import { createSdk } from "../utils"
|
||||
|
||||
const isBash = (part: unknown): part is ToolPart => {
|
||||
if (!part || typeof part !== "object") return false
|
||||
if (!("type" in part) || part.type !== "tool") return false
|
||||
if (!("tool" in part) || part.tool !== "bash") return false
|
||||
return "state" in part
|
||||
}
|
||||
|
||||
test("shell mode runs a command in the project directory", async ({ page, withProject }) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
await withProject(async ({ directory, gotoSession, trackSession }) => {
|
||||
const sdk = createSdk(directory)
|
||||
const prompt = page.locator(promptSelector)
|
||||
const cmd = process.platform === "win32" ? "dir" : "ls"
|
||||
|
||||
await gotoSession()
|
||||
await prompt.click()
|
||||
await page.keyboard.type("!")
|
||||
await expect(prompt).toHaveAttribute("aria-label", /enter shell command/i)
|
||||
|
||||
await page.keyboard.type(cmd)
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
|
||||
|
||||
const id = sessionIDFromUrl(page.url())
|
||||
if (!id) throw new Error(`Failed to parse session id from url: ${page.url()}`)
|
||||
trackSession(id, directory)
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const list = await sdk.session.messages({ sessionID: id, limit: 50 }).then((x) => x.data ?? [])
|
||||
const msg = list.findLast(
|
||||
(item) => item.info.role === "assistant" && "path" in item.info && item.info.path.cwd === directory,
|
||||
)
|
||||
if (!msg) return
|
||||
|
||||
const part = msg.parts
|
||||
.filter(isBash)
|
||||
.find((item) => item.state.input?.command === cmd && item.state.status === "completed")
|
||||
|
||||
if (!part || part.state.status !== "completed") return
|
||||
const output =
|
||||
typeof part.state.metadata?.output === "string" ? part.state.metadata.output : part.state.output
|
||||
if (!output.includes("README.md")) return
|
||||
|
||||
return { cwd: directory, output }
|
||||
},
|
||||
{ timeout: 90_000 },
|
||||
)
|
||||
.toEqual(expect.objectContaining({ cwd: directory, output: expect.stringContaining("README.md") }))
|
||||
|
||||
await expect(prompt).toHaveText("")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector } from "../selectors"
|
||||
import { withSession } from "../actions"
|
||||
|
||||
const shareDisabled = process.env.KILO_DISABLE_SHARE === "true" || process.env.KILO_DISABLE_SHARE === "1"
|
||||
|
||||
async function seed(sdk: Parameters<typeof withSession>[0], sessionID: string) {
|
||||
await sdk.session.promptAsync({
|
||||
sessionID,
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "e2e share seed" }],
|
||||
})
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? [])
|
||||
return messages.length
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
test("/share and /unshare update session share state", async ({ page, sdk, gotoSession }) => {
|
||||
test.skip(shareDisabled, "Share is disabled in this environment (KILO_DISABLE_SHARE).")
|
||||
|
||||
await withSession(sdk, `e2e slash share ${Date.now()}`, async (session) => {
|
||||
const prompt = page.locator(promptSelector)
|
||||
|
||||
await seed(sdk, session.id)
|
||||
await gotoSession(session.id)
|
||||
|
||||
await prompt.click()
|
||||
await page.keyboard.type("/share")
|
||||
await expect(page.locator('[data-slash-id="session.share"]').first()).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
|
||||
return data?.share?.url || undefined
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.not.toBeUndefined()
|
||||
|
||||
await prompt.click()
|
||||
await page.keyboard.type("/unshare")
|
||||
await expect(page.locator('[data-slash-id="session.unshare"]').first()).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
|
||||
return data?.share?.url || undefined
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector } from "../selectors"
|
||||
import { sessionIDFromUrl, withSession } from "../actions"
|
||||
import { cleanupSession, sessionIDFromUrl, withSession } from "../actions"
|
||||
|
||||
test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession }) => {
|
||||
test.setTimeout(120_000)
|
||||
@@ -46,7 +46,7 @@ test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession })
|
||||
.toContain(token)
|
||||
} finally {
|
||||
page.off("pageerror", onPageError)
|
||||
await sdk.session.delete({ sessionID }).catch(() => undefined)
|
||||
await cleanupSession({ sdk, sessionID })
|
||||
}
|
||||
|
||||
if (pageErrors.length > 0) {
|
||||
|
||||
@@ -30,8 +30,6 @@ export const sidebarNavSelector = '[data-component="sidebar-nav-desktop"]'
|
||||
export const projectSwitchSelector = (slug: string) =>
|
||||
`${sidebarNavSelector} [data-action="project-switch"][data-project="${slug}"]`
|
||||
|
||||
export const projectCloseHoverSelector = (slug: string) => `[data-action="project-close-hover"][data-project="${slug}"]`
|
||||
|
||||
export const projectMenuTriggerSelector = (slug: string) =>
|
||||
`${sidebarNavSelector} [data-action="project-menu"][data-project="${slug}"]`
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { seedSessionTask, withSession } from "../actions"
|
||||
import { test, expect } from "../fixtures"
|
||||
|
||||
test("task tool child-session link does not trigger stale show errors", async ({ page, sdk, gotoSession }) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
const errs: string[] = []
|
||||
const onError = (err: Error) => {
|
||||
errs.push(err.message)
|
||||
}
|
||||
page.on("pageerror", onError)
|
||||
|
||||
await withSession(sdk, `e2e child nav ${Date.now()}`, async (session) => {
|
||||
const child = await seedSessionTask(sdk, {
|
||||
sessionID: session.id,
|
||||
description: "Open child session",
|
||||
prompt: "Search the repository for AssistantParts and then reply with exactly CHILD_OK.",
|
||||
})
|
||||
|
||||
try {
|
||||
await gotoSession(session.id)
|
||||
|
||||
const link = page
|
||||
.locator("a.subagent-link")
|
||||
.filter({ hasText: /open child session/i })
|
||||
.first()
|
||||
await expect(link).toBeVisible({ timeout: 30_000 })
|
||||
await link.click()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 })
|
||||
await page.waitForTimeout(1000)
|
||||
expect(errs).toEqual([])
|
||||
} finally {
|
||||
page.off("pageerror", onError)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { clearSessionDockSeed, seedSessionQuestion, seedSessionTodos } from "../actions"
|
||||
import { cleanupSession, clearSessionDockSeed, seedSessionQuestion, seedSessionTodos } from "../actions"
|
||||
import {
|
||||
permissionDockSelector,
|
||||
promptSelector,
|
||||
@@ -26,7 +26,7 @@ async function withDockSession<T>(
|
||||
try {
|
||||
return await fn(session)
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
|
||||
await cleanupSession({ sdk, sessionID: session.id })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ test("child session question request blocks parent dock and unblocks after submi
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
})
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID: child.id }).catch(() => undefined)
|
||||
await cleanupSession({ sdk, sessionID: child.id })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -362,7 +362,7 @@ test("child session permission request blocks parent dock and supports allow onc
|
||||
},
|
||||
)
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID: child.id }).catch(() => undefined)
|
||||
await cleanupSession({ sdk, sessionID: child.id })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@ async function seedConversation(input: {
|
||||
.toBe(true)
|
||||
|
||||
if (!userMessageID) throw new Error("Expected a user message id")
|
||||
await expect(input.page.locator(`[data-message-id="${userMessageID}"]`).first()).toBeVisible({ timeout: 30_000 })
|
||||
await expect(input.page.locator(`[data-message-id="${userMessageID}"]`)).toHaveCount(1, { timeout: 30_000 })
|
||||
return { prompt, userMessageID }
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ test("slash redo clears revert and restores latest state", async ({ page, withPr
|
||||
.toBeUndefined()
|
||||
|
||||
await expect(seeded.prompt).not.toContainText(token)
|
||||
await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`).first()).toBeVisible()
|
||||
await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -161,8 +161,8 @@ test("slash undo/redo traverses multi-step revert stack", async ({ page, withPro
|
||||
const firstMessage = page.locator(`[data-message-id="${first.userMessageID}"]`)
|
||||
const secondMessage = page.locator(`[data-message-id="${second.userMessageID}"]`)
|
||||
|
||||
await expect(firstMessage.first()).toBeVisible()
|
||||
await expect(secondMessage.first()).toBeVisible()
|
||||
await expect(firstMessage).toHaveCount(1)
|
||||
await expect(secondMessage).toHaveCount(1)
|
||||
|
||||
await second.prompt.click()
|
||||
await page.keyboard.press(`${modKey}+A`)
|
||||
@@ -179,7 +179,7 @@ test("slash undo/redo traverses multi-step revert stack", async ({ page, withPro
|
||||
})
|
||||
.toBe(second.userMessageID)
|
||||
|
||||
await expect(firstMessage.first()).toBeVisible()
|
||||
await expect(firstMessage).toHaveCount(1)
|
||||
await expect(secondMessage).toHaveCount(0)
|
||||
|
||||
await second.prompt.click()
|
||||
@@ -213,7 +213,7 @@ test("slash undo/redo traverses multi-step revert stack", async ({ page, withPro
|
||||
})
|
||||
.toBe(second.userMessageID)
|
||||
|
||||
await expect(firstMessage.first()).toBeVisible()
|
||||
await expect(firstMessage).toHaveCount(1)
|
||||
await expect(secondMessage).toHaveCount(0)
|
||||
|
||||
await second.prompt.click()
|
||||
@@ -229,8 +229,8 @@ test("slash undo/redo traverses multi-step revert stack", async ({ page, withPro
|
||||
})
|
||||
.toBeUndefined()
|
||||
|
||||
await expect(firstMessage.first()).toBeVisible()
|
||||
await expect(secondMessage.first()).toBeVisible()
|
||||
await expect(firstMessage).toHaveCount(1)
|
||||
await expect(secondMessage).toHaveCount(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,22 +32,19 @@ test("changing sidebar toggle keybind works", async ({ page, gotoSession }) => {
|
||||
|
||||
await closeDialog(page, dialog)
|
||||
|
||||
const main = page.locator("main")
|
||||
const initialClasses = (await main.getAttribute("class")) ?? ""
|
||||
const initiallyClosed = initialClasses.includes("xl:border-l")
|
||||
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||
const initiallyClosed = (await button.getAttribute("aria-expanded")) !== "true"
|
||||
|
||||
await page.keyboard.press(`${modKey}+Shift+H`)
|
||||
await page.waitForTimeout(100)
|
||||
await expect(button).toHaveAttribute("aria-expanded", initiallyClosed ? "true" : "false")
|
||||
|
||||
const afterToggleClasses = (await main.getAttribute("class")) ?? ""
|
||||
const afterToggleClosed = afterToggleClasses.includes("xl:border-l")
|
||||
const afterToggleClosed = (await button.getAttribute("aria-expanded")) !== "true"
|
||||
expect(afterToggleClosed).toBe(!initiallyClosed)
|
||||
|
||||
await page.keyboard.press(`${modKey}+Shift+H`)
|
||||
await page.waitForTimeout(100)
|
||||
await expect(button).toHaveAttribute("aria-expanded", initiallyClosed ? "false" : "true")
|
||||
|
||||
const finalClasses = (await main.getAttribute("class")) ?? ""
|
||||
const finalClosed = finalClasses.includes("xl:border-l")
|
||||
const finalClosed = (await button.getAttribute("aria-expanded")) !== "true"
|
||||
expect(finalClosed).toBe(initiallyClosed)
|
||||
})
|
||||
|
||||
|
||||
@@ -83,16 +83,23 @@ test("changing theme persists in localStorage", async ({ page, gotoSession }) =>
|
||||
const select = dialog.locator(settingsThemeSelector)
|
||||
await expect(select).toBeVisible()
|
||||
|
||||
const currentThemeId = await page.evaluate(() => {
|
||||
return document.documentElement.getAttribute("data-theme")
|
||||
})
|
||||
const currentTheme = (await select.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
|
||||
|
||||
await select.locator('[data-slot="select-select-trigger"]').click()
|
||||
|
||||
const items = page.locator('[data-slot="select-select-item"]')
|
||||
const count = await items.count()
|
||||
expect(count).toBeGreaterThan(1)
|
||||
|
||||
const firstTheme = await items.nth(1).locator('[data-slot="select-select-item-label"]').textContent()
|
||||
expect(firstTheme).toBeTruthy()
|
||||
const nextTheme = (await items.locator('[data-slot="select-select-item-label"]').allTextContents())
|
||||
.map((x) => x.trim())
|
||||
.find((x) => x && x !== currentTheme)
|
||||
expect(nextTheme).toBeTruthy()
|
||||
|
||||
await items.nth(1).click()
|
||||
await items.filter({ hasText: nextTheme! }).first().click()
|
||||
|
||||
await page.keyboard.press("Escape")
|
||||
|
||||
@@ -101,7 +108,7 @@ test("changing theme persists in localStorage", async ({ page, gotoSession }) =>
|
||||
})
|
||||
|
||||
expect(storedThemeId).not.toBeNull()
|
||||
expect(storedThemeId).not.toBe("oc-1")
|
||||
expect(storedThemeId).not.toBe(currentThemeId)
|
||||
|
||||
const dataTheme = await page.evaluate(() => {
|
||||
return document.documentElement.getAttribute("data-theme")
|
||||
@@ -109,6 +116,42 @@ test("changing theme persists in localStorage", async ({ page, gotoSession }) =>
|
||||
expect(dataTheme).toBe(storedThemeId)
|
||||
})
|
||||
|
||||
test("legacy oc-1 theme migrates to oc-2", async ({ page, gotoSession }) => {
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem("opencode-theme-id", "oc-1")
|
||||
localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;")
|
||||
localStorage.setItem("opencode-theme-css-dark", "--background-base:#000;")
|
||||
})
|
||||
|
||||
await gotoSession()
|
||||
|
||||
await expect(page.locator("html")).toHaveAttribute("data-theme", "oc-2")
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await page.evaluate(() => {
|
||||
return localStorage.getItem("opencode-theme-id")
|
||||
})
|
||||
})
|
||||
.toBe("oc-2")
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await page.evaluate(() => {
|
||||
return localStorage.getItem("opencode-theme-css-light")
|
||||
})
|
||||
})
|
||||
.toBeNull()
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await page.evaluate(() => {
|
||||
return localStorage.getItem("opencode-theme-css-dark")
|
||||
})
|
||||
})
|
||||
.toBeNull()
|
||||
})
|
||||
|
||||
test("changing font persists in localStorage and updates CSS variable", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { closeSidebar, hoverSessionItem } from "../actions"
|
||||
import { projectSwitchSelector, sessionItemSelector } from "../selectors"
|
||||
import { cleanupSession, closeSidebar, hoverSessionItem } from "../actions"
|
||||
import { projectSwitchSelector } from "../selectors"
|
||||
|
||||
test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => {
|
||||
const stamp = Date.now()
|
||||
@@ -15,12 +15,15 @@ test("collapsed sidebar popover stays open when archiving a session", async ({ p
|
||||
await gotoSession(one.id)
|
||||
await closeSidebar(page)
|
||||
|
||||
const oneItem = page.locator(`[data-session-id="${one.id}"]`).last()
|
||||
const twoItem = page.locator(`[data-session-id="${two.id}"]`).last()
|
||||
|
||||
const project = page.locator(projectSwitchSelector(slug)).first()
|
||||
await expect(project).toBeVisible()
|
||||
await project.hover()
|
||||
|
||||
await expect(page.locator(sessionItemSelector(one.id)).first()).toBeVisible()
|
||||
await expect(page.locator(sessionItemSelector(two.id)).first()).toBeVisible()
|
||||
await expect(oneItem).toBeVisible()
|
||||
await expect(twoItem).toBeVisible()
|
||||
|
||||
const item = await hoverSessionItem(page, one.id)
|
||||
await item
|
||||
@@ -28,9 +31,9 @@ test("collapsed sidebar popover stays open when archiving a session", async ({ p
|
||||
.first()
|
||||
.click()
|
||||
|
||||
await expect(page.locator(sessionItemSelector(two.id)).first()).toBeVisible()
|
||||
await expect(twoItem).toBeVisible()
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID: one.id }).catch(() => undefined)
|
||||
await sdk.session.delete({ sessionID: two.id }).catch(() => undefined)
|
||||
await cleanupSession({ sdk, sessionID: one.id })
|
||||
await cleanupSession({ sdk, sessionID: two.id })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { openSidebar, withSession } from "../actions"
|
||||
import { cleanupSession, openSidebar, withSession } from "../actions"
|
||||
import { promptSelector } from "../selectors"
|
||||
|
||||
test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }) => {
|
||||
@@ -18,14 +18,13 @@ test("sidebar session links navigate to the selected session", async ({ page, sl
|
||||
|
||||
const target = page.locator(`[data-session-id="${two.id}"] a`).first()
|
||||
await expect(target).toBeVisible()
|
||||
await target.scrollIntoViewIfNeeded()
|
||||
await target.click()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
await expect(page.locator(`[data-session-id="${two.id}"] a`).first()).toHaveClass(/\bactive\b/)
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID: one.id }).catch(() => undefined)
|
||||
await sdk.session.delete({ sessionID: two.id }).catch(() => undefined)
|
||||
await cleanupSession({ sdk, sessionID: one.id })
|
||||
await cleanupSession({ sdk, sessionID: two.id })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,12 +5,14 @@ test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await openSidebar(page)
|
||||
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||
await expect(button).toHaveAttribute("aria-expanded", "true")
|
||||
|
||||
await toggleSidebar(page)
|
||||
await expect(page.locator("main")).toHaveClass(/xl:border-l/)
|
||||
await expect(button).toHaveAttribute("aria-expanded", "false")
|
||||
|
||||
await toggleSidebar(page)
|
||||
await expect(page.locator("main")).not.toHaveClass(/xl:border-l/)
|
||||
await expect(button).toHaveAttribute("aria-expanded", "true")
|
||||
})
|
||||
|
||||
test("sidebar collapsed state persists across navigation and reload", async ({ page, sdk, gotoSession }) => {
|
||||
@@ -19,14 +21,15 @@ test("sidebar collapsed state persists across navigation and reload", async ({ p
|
||||
await gotoSession(session1.id)
|
||||
|
||||
await openSidebar(page)
|
||||
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||
await toggleSidebar(page)
|
||||
await expect(page.locator("main")).toHaveClass(/xl:border-l/)
|
||||
await expect(button).toHaveAttribute("aria-expanded", "false")
|
||||
|
||||
await gotoSession(session2.id)
|
||||
await expect(page.locator("main")).toHaveClass(/xl:border-l/)
|
||||
await expect(button).toHaveAttribute("aria-expanded", "false")
|
||||
|
||||
await page.reload()
|
||||
await expect(page.locator("main")).toHaveClass(/xl:border-l/)
|
||||
await expect(button).toHaveAttribute("aria-expanded", "false")
|
||||
|
||||
const opened = await page.evaluate(
|
||||
() => JSON.parse(localStorage.getItem("opencode.global.dat:layout") ?? "{}").sidebar?.opened,
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { Page } from "@playwright/test"
|
||||
import { test, expect } from "../fixtures"
|
||||
import { terminalSelector } from "../selectors"
|
||||
import { terminalToggleKey, workspacePersistKey } from "../utils"
|
||||
|
||||
type State = {
|
||||
active?: string
|
||||
all: Array<{
|
||||
id: string
|
||||
title: string
|
||||
titleNumber: number
|
||||
buffer?: string
|
||||
}>
|
||||
}
|
||||
|
||||
async function open(page: Page) {
|
||||
const terminal = page.locator(terminalSelector)
|
||||
const visible = await terminal.isVisible().catch(() => false)
|
||||
if (!visible) await page.keyboard.press(terminalToggleKey)
|
||||
await expect(terminal).toBeVisible()
|
||||
await expect(terminal.locator("textarea")).toHaveCount(1)
|
||||
}
|
||||
|
||||
async function run(page: Page, cmd: string) {
|
||||
const terminal = page.locator(terminalSelector)
|
||||
await expect(terminal).toBeVisible()
|
||||
await terminal.click()
|
||||
await page.keyboard.type(cmd)
|
||||
await page.keyboard.press("Enter")
|
||||
}
|
||||
|
||||
async function store(page: Page, key: string) {
|
||||
return page.evaluate((key) => {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (raw) return JSON.parse(raw) as State
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const next = localStorage.key(i)
|
||||
if (!next?.endsWith(":workspace:terminal")) continue
|
||||
const value = localStorage.getItem(next)
|
||||
if (!value) continue
|
||||
return JSON.parse(value) as State
|
||||
}
|
||||
}, key)
|
||||
}
|
||||
|
||||
test("inactive terminal tab buffers persist across tab switches", async ({ page, withProject }) => {
|
||||
await withProject(async ({ directory, gotoSession }) => {
|
||||
const key = workspacePersistKey(directory, "terminal")
|
||||
const one = `E2E_TERM_ONE_${Date.now()}`
|
||||
const two = `E2E_TERM_TWO_${Date.now()}`
|
||||
const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')
|
||||
const first = tabs.filter({ hasText: /Terminal 1/ }).first()
|
||||
const second = tabs.filter({ hasText: /Terminal 2/ }).first()
|
||||
|
||||
await gotoSession()
|
||||
await open(page)
|
||||
|
||||
await run(page, `echo ${one}`)
|
||||
|
||||
await page.getByRole("button", { name: /new terminal/i }).click()
|
||||
await expect(tabs).toHaveCount(2)
|
||||
|
||||
await run(page, `echo ${two}`)
|
||||
|
||||
await first.click()
|
||||
await expect(first).toHaveAttribute("aria-selected", "true")
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const state = await store(page, key)
|
||||
const first = state?.all.find((item) => item.titleNumber === 1)?.buffer ?? ""
|
||||
const second = state?.all.find((item) => item.titleNumber === 2)?.buffer ?? ""
|
||||
return {
|
||||
first: first.includes(one),
|
||||
second: second.includes(two),
|
||||
}
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toEqual({ first: false, second: true })
|
||||
|
||||
await second.click()
|
||||
await expect(second).toHaveAttribute("aria-selected", "true")
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const state = await store(page, key)
|
||||
const first = state?.all.find((item) => item.titleNumber === 1)?.buffer ?? ""
|
||||
const second = state?.all.find((item) => item.titleNumber === 2)?.buffer ?? ""
|
||||
return {
|
||||
first: first.includes(one),
|
||||
second: second.includes(two),
|
||||
}
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toEqual({ first: true, second: false })
|
||||
})
|
||||
})
|
||||
|
||||
test("closing the active terminal tab falls back to the previous tab", async ({ page, withProject }) => {
|
||||
await withProject(async ({ directory, gotoSession }) => {
|
||||
const key = workspacePersistKey(directory, "terminal")
|
||||
const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')
|
||||
|
||||
await gotoSession()
|
||||
await open(page)
|
||||
|
||||
await page.getByRole("button", { name: /new terminal/i }).click()
|
||||
await expect(tabs).toHaveCount(2)
|
||||
|
||||
const second = tabs.filter({ hasText: /Terminal 2/ }).first()
|
||||
await second.click()
|
||||
await expect(second).toHaveAttribute("aria-selected", "true")
|
||||
|
||||
await second.hover()
|
||||
await page
|
||||
.getByRole("button", { name: /close terminal/i })
|
||||
.nth(1)
|
||||
.click({ force: true })
|
||||
|
||||
const first = tabs.filter({ hasText: /Terminal 1/ }).first()
|
||||
await expect(tabs).toHaveCount(1)
|
||||
await expect(first).toHaveAttribute("aria-selected", "true")
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const state = await store(page, key)
|
||||
return {
|
||||
count: state?.all.length ?? 0,
|
||||
first: state?.all.some((item) => item.titleNumber === 1) ?? false,
|
||||
}
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
.toEqual({ count: 1, first: true })
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createKiloClient } from "@kilocode/sdk/v2/client"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { base64Encode, checksum } from "@opencode-ai/util/encode"
|
||||
|
||||
export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"
|
||||
export const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||
@@ -7,6 +7,22 @@ export const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||
export const serverUrl = `http://${serverHost}:${serverPort}`
|
||||
export const serverName = `${serverHost}:${serverPort}`
|
||||
|
||||
const localHosts = ["127.0.0.1", "localhost"]
|
||||
|
||||
const serverLabels = (() => {
|
||||
const url = new URL(serverUrl)
|
||||
if (!localHosts.includes(url.hostname)) return [serverName]
|
||||
return localHosts.map((host) => `${host}:${url.port}`)
|
||||
})()
|
||||
|
||||
export const serverNames = [...new Set(serverLabels)]
|
||||
|
||||
export const serverUrls = serverNames.map((name) => `http://${name}`)
|
||||
|
||||
const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
|
||||
export const serverNamePattern = new RegExp(`(?:${serverNames.map(escape).join("|")})`)
|
||||
|
||||
export const modKey = process.platform === "darwin" ? "Meta" : "Control"
|
||||
export const terminalToggleKey = "Control+Backquote"
|
||||
|
||||
@@ -14,6 +30,12 @@ export function createSdk(directory?: string) {
|
||||
return createKiloClient({ baseUrl: serverUrl, directory, throwOnError: true })
|
||||
}
|
||||
|
||||
export async function resolveDirectory(directory: string) {
|
||||
return createSdk(directory)
|
||||
.path.get()
|
||||
.then((x) => x.data?.directory ?? directory)
|
||||
}
|
||||
|
||||
export async function getWorktree() {
|
||||
const sdk = createSdk()
|
||||
const result = await sdk.path.get()
|
||||
@@ -33,3 +55,9 @@ export function dirPath(directory: string) {
|
||||
export function sessionPath(directory: string, sessionID?: string) {
|
||||
return `${dirPath(directory)}/session${sessionID ? `/${sessionID}` : ""}`
|
||||
}
|
||||
|
||||
export function workspacePersistKey(directory: string, key: string) {
|
||||
const head = (directory.slice(0, 12) || "workspace").replace(/[^a-zA-Z0-9._-]/g, "-")
|
||||
const sum = checksum(directory) ?? "0"
|
||||
return `opencode.workspace.${head}.${sum}.dat:workspace:${key}`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "7.0.47",
|
||||
"version": "7.2.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -57,7 +57,7 @@
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"ghostty-web": "0.4.0",
|
||||
"ghostty-web": "github:anomalyco/ghostty-web#main",
|
||||
"luxon": "catalog:",
|
||||
"marked": "catalog:",
|
||||
"marked-shiki": "catalog:",
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
;(function () {
|
||||
var themeId = localStorage.getItem("opencode-theme-id")
|
||||
if (!themeId) return
|
||||
var key = "opencode-theme-id"
|
||||
var themeId = localStorage.getItem(key) || "oc-2"
|
||||
|
||||
if (themeId === "oc-1") {
|
||||
themeId = "oc-2"
|
||||
localStorage.setItem(key, themeId)
|
||||
localStorage.removeItem("opencode-theme-css-light")
|
||||
localStorage.removeItem("opencode-theme-css-dark")
|
||||
}
|
||||
|
||||
var scheme = localStorage.getItem("opencode-color-scheme") || "system"
|
||||
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
@@ -9,9 +16,9 @@
|
||||
document.documentElement.dataset.theme = themeId
|
||||
document.documentElement.dataset.colorScheme = mode
|
||||
|
||||
if (themeId === "oc-1") return
|
||||
if (themeId === "oc-2") return
|
||||
|
||||
var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
|
||||
var css = localStorage.getItem("opencode-theme-css-" + mode)
|
||||
if (css) {
|
||||
var style = document.createElement("style")
|
||||
style.id = "oc-theme-preload"
|
||||
|
||||
@@ -244,7 +244,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
draggingType: "image" | "@mention" | null
|
||||
mode: "normal" | "shell"
|
||||
applyingHistory: boolean
|
||||
pendingAutoAccept: boolean
|
||||
}>({
|
||||
popover: null,
|
||||
historyIndex: -1,
|
||||
@@ -253,7 +252,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
draggingType: null,
|
||||
mode: "normal",
|
||||
applyingHistory: false,
|
||||
pendingAutoAccept: false,
|
||||
})
|
||||
|
||||
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
||||
@@ -306,12 +304,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(sessionKey, () => {
|
||||
setStore("pendingAutoAccept", false)
|
||||
}),
|
||||
)
|
||||
|
||||
const historyComments = () => {
|
||||
const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const))
|
||||
return prompt.context.items().flatMap((item) => {
|
||||
@@ -961,7 +953,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const variants = createMemo(() => ["default", ...local.model.variant.list()])
|
||||
const accepting = createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return store.pendingAutoAccept
|
||||
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
|
||||
return permission.isAutoAccepting(id, sdk.directory)
|
||||
})
|
||||
|
||||
@@ -1215,9 +1207,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
aria-multiline="true"
|
||||
aria-label={placeholder()}
|
||||
contenteditable="true"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellcheck={false}
|
||||
autocapitalize={store.mode === "normal" ? "sentences" : "off"}
|
||||
autocorrect={store.mode === "normal" ? "on" : "off"}
|
||||
spellcheck={store.mode === "normal"}
|
||||
onInput={handleInput}
|
||||
onPaste={handlePaste}
|
||||
onCompositionStart={() => setComposing(true)}
|
||||
@@ -1340,7 +1332,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
if (!params.id) {
|
||||
setStore("pendingAutoAccept", (value) => !value)
|
||||
permission.toggleAutoAcceptDirectory(sdk.directory)
|
||||
return
|
||||
}
|
||||
permission.toggleAutoAccept(params.id, sdk.directory)
|
||||
|
||||
@@ -6,10 +6,19 @@ let createPromptSubmit: typeof import("./submit").createPromptSubmit
|
||||
const createdClients: string[] = []
|
||||
const createdSessions: string[] = []
|
||||
const enabledAutoAccept: Array<{ sessionID: string; directory: string }> = []
|
||||
const optimistic: Array<{
|
||||
message: {
|
||||
agent: string
|
||||
model: { providerID: string; modelID: string }
|
||||
variant?: string
|
||||
}
|
||||
}> = []
|
||||
const sentShell: string[] = []
|
||||
const syncedDirectories: string[] = []
|
||||
|
||||
let params: { id?: string } = {}
|
||||
let selected = "/repo/worktree-a"
|
||||
let variant: string | undefined
|
||||
|
||||
const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
|
||||
|
||||
@@ -26,6 +35,7 @@ const clientFor = (directory: string) => {
|
||||
return { data: undefined }
|
||||
},
|
||||
prompt: async () => ({ data: undefined }),
|
||||
promptAsync: async () => ({ data: undefined }),
|
||||
command: async () => ({ data: undefined }),
|
||||
abort: async () => ({ data: undefined }),
|
||||
},
|
||||
@@ -40,7 +50,7 @@ beforeAll(async () => {
|
||||
|
||||
mock.module("@solidjs/router", () => ({
|
||||
useNavigate: () => () => undefined,
|
||||
useParams: () => ({}),
|
||||
useParams: () => params,
|
||||
}))
|
||||
|
||||
mock.module("@kilocode/sdk/v2/client", () => ({
|
||||
@@ -62,7 +72,7 @@ beforeAll(async () => {
|
||||
useLocal: () => ({
|
||||
model: {
|
||||
current: () => ({ id: "model", provider: { id: "provider" } }),
|
||||
variant: { current: () => undefined },
|
||||
variant: { current: () => variant },
|
||||
},
|
||||
agent: {
|
||||
current: () => ({ name: "agent" }),
|
||||
@@ -118,7 +128,11 @@ beforeAll(async () => {
|
||||
data: { command: [] },
|
||||
session: {
|
||||
optimistic: {
|
||||
add: () => undefined,
|
||||
add: (value: {
|
||||
message: { agent: string; model: { providerID: string; modelID: string }; variant?: string }
|
||||
}) => {
|
||||
optimistic.push(value)
|
||||
},
|
||||
remove: () => undefined,
|
||||
},
|
||||
},
|
||||
@@ -155,9 +169,12 @@ beforeEach(() => {
|
||||
createdClients.length = 0
|
||||
createdSessions.length = 0
|
||||
enabledAutoAccept.length = 0
|
||||
optimistic.length = 0
|
||||
params = {}
|
||||
sentShell.length = 0
|
||||
syncedDirectories.length = 0
|
||||
selected = "/repo/worktree-a"
|
||||
variant = undefined
|
||||
})
|
||||
|
||||
describe("prompt submit worktree selection", () => {
|
||||
@@ -219,4 +236,39 @@ describe("prompt submit worktree selection", () => {
|
||||
|
||||
expect(enabledAutoAccept).toEqual([{ sessionID: "session-1", directory: "/repo/worktree-a" }])
|
||||
})
|
||||
|
||||
test("includes the selected variant on optimistic prompts", async () => {
|
||||
params = { id: "session-1" }
|
||||
variant = "high"
|
||||
|
||||
const submit = createPromptSubmit({
|
||||
info: () => ({ id: "session-1" }),
|
||||
imageAttachments: () => [],
|
||||
commentCount: () => 0,
|
||||
autoAccept: () => false,
|
||||
mode: () => "normal",
|
||||
working: () => false,
|
||||
editor: () => undefined,
|
||||
queueScroll: () => undefined,
|
||||
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||
addToHistory: () => undefined,
|
||||
resetHistoryNavigation: () => undefined,
|
||||
setMode: () => undefined,
|
||||
setPopover: () => undefined,
|
||||
onSubmit: () => undefined,
|
||||
})
|
||||
|
||||
const event = { preventDefault: () => undefined } as unknown as Event
|
||||
|
||||
await submit.handleSubmit(event)
|
||||
|
||||
expect(optimistic).toHaveLength(1)
|
||||
expect(optimistic[0]).toMatchObject({
|
||||
message: {
|
||||
agent: "agent",
|
||||
model: { providerID: "provider", modelID: "model" },
|
||||
variant: "high",
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Identifier } from "@/utils/id"
|
||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||
import { buildRequestParts } from "./build-request-parts"
|
||||
import { setCursorPosition } from "./editor-dom"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
|
||||
type PendingPrompt = {
|
||||
abort: AbortController
|
||||
@@ -286,7 +287,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.commandSendFailed.title"),
|
||||
description: errorMessage(err),
|
||||
description: formatServerError(err, language.t, language.t("common.requestFailed")),
|
||||
})
|
||||
restoreInput()
|
||||
})
|
||||
@@ -315,6 +316,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
time: { created: Date.now() },
|
||||
agent,
|
||||
model,
|
||||
variant,
|
||||
}
|
||||
|
||||
const addOptimisticMessage = () =>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
|
||||
const usd = createMemo(
|
||||
() =>
|
||||
new Intl.NumberFormat(language.locale(), {
|
||||
new Intl.NumberFormat(language.intl(), {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}),
|
||||
@@ -77,7 +77,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
{(ctx) => (
|
||||
<>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-text-invert-strong">{ctx().total.toLocaleString(language.locale())}</span>
|
||||
<span class="text-text-invert-strong">{ctx().total.toLocaleString(language.intl())}</span>
|
||||
<span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -128,7 +128,7 @@ export function SessionContextTab() {
|
||||
|
||||
const usd = createMemo(
|
||||
() =>
|
||||
new Intl.NumberFormat(language.locale(), {
|
||||
new Intl.NumberFormat(language.intl(), {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}),
|
||||
@@ -136,7 +136,7 @@ export function SessionContextTab() {
|
||||
|
||||
const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all))
|
||||
const ctx = createMemo(() => metrics().context)
|
||||
const formatter = createMemo(() => createSessionContextFormatter(language.locale()))
|
||||
const formatter = createMemo(() => createSessionContextFormatter(language.intl()))
|
||||
|
||||
const cost = createMemo(() => {
|
||||
return usd().format(metrics().totalCost)
|
||||
@@ -200,7 +200,7 @@ export function SessionContextTab() {
|
||||
|
||||
const stats = [
|
||||
{ label: "context.stats.session", value: () => info()?.title ?? params.id ?? "—" },
|
||||
{ label: "context.stats.messages", value: () => counts().all.toLocaleString(language.locale()) },
|
||||
{ label: "context.stats.messages", value: () => counts().all.toLocaleString(language.intl()) },
|
||||
{ label: "context.stats.provider", value: providerLabel },
|
||||
{ label: "context.stats.model", value: modelLabel },
|
||||
{ label: "context.stats.limit", value: () => formatter().number(ctx()?.limit) },
|
||||
@@ -213,8 +213,8 @@ export function SessionContextTab() {
|
||||
label: "context.stats.cacheTokens",
|
||||
value: () => `${formatter().number(ctx()?.cacheRead)} / ${formatter().number(ctx()?.cacheWrite)}`,
|
||||
},
|
||||
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.locale()) },
|
||||
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.locale()) },
|
||||
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.intl()) },
|
||||
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.intl()) },
|
||||
{ label: "context.stats.totalCost", value: cost },
|
||||
{ label: "context.stats.sessionCreated", value: () => formatter().time(info()?.time.created) },
|
||||
{ label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
|
||||
@@ -307,7 +307,7 @@ export function SessionContextTab() {
|
||||
<div class="flex items-center gap-1 text-11-regular text-text-weak">
|
||||
<div class="size-2 rounded-sm" style={{ "background-color": BREAKDOWN_COLOR[segment.key] }} />
|
||||
<div>{breakdownLabel(segment.key)}</div>
|
||||
<div class="text-text-weaker">{segment.percent.toLocaleString(language.locale())}%</div>
|
||||
<div class="text-text-weaker">{segment.percent.toLocaleString(language.intl())}%</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -303,7 +303,12 @@ export function SessionHeader() {
|
||||
})
|
||||
|
||||
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
||||
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
|
||||
const current = createMemo(
|
||||
() =>
|
||||
options().find((o) => o.id === prefs.app) ??
|
||||
options()[0] ??
|
||||
({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const),
|
||||
)
|
||||
const opening = createMemo(() => openRequest.app !== undefined)
|
||||
|
||||
const selectApp = (app: OpenApp) => {
|
||||
|
||||
@@ -4,12 +4,12 @@ import { useSync } from "@/context/sync"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
|
||||
const MAIN_WORKTREE = "main"
|
||||
const CREATE_WORKTREE = "create"
|
||||
const ROOT_CLASS =
|
||||
"size-full flex flex-col justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto 2xl:max-w-[1000px] px-6 pb-16"
|
||||
const ROOT_CLASS = "size-full flex flex-col"
|
||||
|
||||
interface NewSessionViewProps {
|
||||
worktree: string
|
||||
@@ -50,33 +50,43 @@ export function NewSessionView(props: NewSessionViewProps) {
|
||||
|
||||
return (
|
||||
<div class={ROOT_CLASS}>
|
||||
<div class="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-12-medium text-text-weak select-text">
|
||||
{getDirectory(projectRoot())}
|
||||
<span class="text-text-strong">{getFilename(projectRoot())}</span>
|
||||
<div class="h-12 shrink-0" aria-hidden />
|
||||
<div class="flex-1 px-6 pb-30 flex items-center justify-center text-center">
|
||||
<div class="w-full max-w-200 flex flex-col items-center text-center gap-4">
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<Mark class="w-10" />
|
||||
<div class="text-20-medium text-text-strong">{language.t("session.new.title")}</div>
|
||||
</div>
|
||||
<div class="w-full flex flex-col gap-4 items-center">
|
||||
<div class="flex items-start justify-center gap-3 min-h-5">
|
||||
<div class="text-12-medium text-text-weak select-text leading-5 min-w-0 max-w-160 break-words text-center">
|
||||
{getDirectory(projectRoot())}
|
||||
<span class="text-text-strong">{getFilename(projectRoot())}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start justify-center gap-1.5 min-h-5">
|
||||
<Icon name="branch" size="small" class="mt-0.5 shrink-0" />
|
||||
<div class="text-12-medium text-text-weak select-text leading-5 min-w-0 max-w-160 break-words text-center">
|
||||
{label(current())}
|
||||
</div>
|
||||
</div>
|
||||
<Show when={sync.project}>
|
||||
{(project) => (
|
||||
<div class="flex items-start justify-center gap-3 min-h-5">
|
||||
<div class="text-12-medium text-text-weak leading-5 min-w-0 max-w-160 break-words text-center">
|
||||
{language.t("session.new.lastModified")}
|
||||
<span class="text-text-strong">
|
||||
{DateTime.fromMillis(project().time.updated ?? project().time.created)
|
||||
.setLocale(language.intl())
|
||||
.toRelative()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center items-center gap-1">
|
||||
<Icon name="branch" size="small" />
|
||||
<div class="text-12-medium text-text-weak select-text ml-2">{label(current())}</div>
|
||||
</div>
|
||||
<Show when={sync.project}>
|
||||
{(project) => (
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="pencil-line" size="small" />
|
||||
<div class="text-12-medium text-text-weak">
|
||||
{language.t("session.new.lastModified")}
|
||||
<span class="text-text-strong">
|
||||
{DateTime.fromMillis(project().time.updated ?? project().time.created)
|
||||
.setLocale(language.locale())
|
||||
.toRelative()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ type ProviderItem = ReturnType<ReturnType<typeof useProviders>["connected"]>[num
|
||||
|
||||
const PROVIDER_NOTES = [
|
||||
{ match: (id: string) => id === "opencode", key: "dialog.provider.opencode.note" },
|
||||
{ match: (id: string) => id === "opencode-go", key: "dialog.provider.opencodeGo.tagline" },
|
||||
{ match: (id: string) => id === "anthropic", key: "dialog.provider.anthropic.note" },
|
||||
{ match: (id: string) => id.startsWith("github-copilot"), key: "dialog.provider.copilot.note" },
|
||||
{ match: (id: string) => id === "openai", key: "dialog.provider.openai.note" },
|
||||
@@ -181,21 +182,11 @@ export const SettingsProviders: Component = () => {
|
||||
<div class="flex items-center gap-x-3">
|
||||
<ProviderIcon id={item.id} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong">{item.name}</span>
|
||||
<Show when={item.id === "opencode"}>
|
||||
<span class="text-14-regular text-text-weak">
|
||||
{language.t("dialog.provider.opencode.tagline")}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={item.id === "opencode"}>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</Show>
|
||||
<Show when={item.id === "opencode-go"}>
|
||||
<>
|
||||
<span class="text-14-regular text-text-weak">
|
||||
{language.t("dialog.provider.opencodeGo.tagline")}
|
||||
</span>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={note(item.id)}>
|
||||
|
||||
@@ -217,7 +217,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
const currentTheme = theme.themes()[theme.themeId()]
|
||||
if (!currentTheme) return fallback
|
||||
const variant = mode === "dark" ? currentTheme.dark : currentTheme.light
|
||||
if (!variant?.seeds) return fallback
|
||||
if (!variant?.seeds && !variant?.palette) return fallback
|
||||
const resolved = resolveThemeVariant(variant, mode === "dark")
|
||||
const text = resolved["text-stronger"] ?? fallback.foreground
|
||||
const background = resolved["background-stronger"] ?? fallback.background
|
||||
|
||||
@@ -155,7 +155,7 @@ export function Titlebar() {
|
||||
|
||||
return (
|
||||
<header
|
||||
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center"
|
||||
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
|
||||
style={{ "min-height": minHeight() }}
|
||||
data-tauri-drag-region
|
||||
onMouseDown={drag}
|
||||
@@ -269,7 +269,7 @@ export function Titlebar() {
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
||||
<div id="opencode-titlebar-center" class="pointer-events-auto w-full min-w-0 flex justify-center lg:w-fit" />
|
||||
<div id="opencode-titlebar-center" class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -27,7 +27,7 @@ import type { InitError } from "../pages/error"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { bootstrapDirectory, bootstrapGlobal } from "./global-sync/bootstrap"
|
||||
import { createChildStoreManager } from "./global-sync/child-store"
|
||||
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
|
||||
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
|
||||
import { createRefreshQueue } from "./global-sync/queue"
|
||||
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
||||
import { trimSessions } from "./global-sync/session-trim"
|
||||
@@ -189,6 +189,7 @@ function createGlobalSync() {
|
||||
})
|
||||
if (next.length !== store.session.length) {
|
||||
setStore("session", reconcile(next, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo)
|
||||
}
|
||||
children.unpin(directory)
|
||||
return
|
||||
@@ -220,6 +221,7 @@ function createGlobalSync() {
|
||||
}),
|
||||
)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||
sessionMeta.set(directory, { limit })
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -228,10 +230,7 @@ function createGlobalSync() {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.listFailed.title", { project }),
|
||||
description: formatServerError(err, {
|
||||
unknown: language.t("error.chain.unknown"),
|
||||
invalidConfiguration: language.t("error.server.invalidConfiguration"),
|
||||
}),
|
||||
description: formatServerError(err, language.t),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -261,8 +260,7 @@ function createGlobalSync() {
|
||||
setStore: child[1],
|
||||
vcsCache: cache,
|
||||
loadSessions,
|
||||
unknownError: language.t("error.chain.unknown"),
|
||||
invalidConfigurationError: language.t("error.server.invalidConfiguration"),
|
||||
translate: language.t,
|
||||
})
|
||||
})()
|
||||
|
||||
@@ -331,8 +329,7 @@ function createGlobalSync() {
|
||||
url: globalSDK.url,
|
||||
}),
|
||||
requestFailedTitle: language.t("common.requestFailed"),
|
||||
unknownError: language.t("error.chain.unknown"),
|
||||
invalidConfigurationError: language.t("error.server.invalidConfiguration"),
|
||||
translate: language.t,
|
||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||
setGlobalStore: setBootStore,
|
||||
})
|
||||
|
||||
@@ -36,8 +36,7 @@ export async function bootstrapGlobal(input: {
|
||||
connectErrorTitle: string
|
||||
connectErrorDescription: string
|
||||
requestFailedTitle: string
|
||||
unknownError: string
|
||||
invalidConfigurationError: string
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
formatMoreCount: (count: number) => string
|
||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||
}) {
|
||||
@@ -91,10 +90,7 @@ export async function bootstrapGlobal(input: {
|
||||
const results = await Promise.allSettled(tasks)
|
||||
const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason)
|
||||
if (errors.length) {
|
||||
const message = formatServerError(errors[0], {
|
||||
unknown: input.unknownError,
|
||||
invalidConfiguration: input.invalidConfigurationError,
|
||||
})
|
||||
const message = formatServerError(errors[0], input.translate)
|
||||
const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : ""
|
||||
showToast({
|
||||
variant: "error",
|
||||
@@ -122,8 +118,7 @@ export async function bootstrapDirectory(input: {
|
||||
setStore: SetStoreFunction<State>
|
||||
vcsCache: VcsCache
|
||||
loadSessions: (directory: string) => Promise<void> | void
|
||||
unknownError: string
|
||||
invalidConfigurationError: string
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
}) {
|
||||
if (input.store.status !== "complete") input.setStore("status", "loading")
|
||||
|
||||
@@ -145,10 +140,7 @@ export async function bootstrapDirectory(input: {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: `Failed to reload ${project}`,
|
||||
description: formatServerError(err, {
|
||||
unknown: input.unknownError,
|
||||
invalidConfiguration: input.invalidConfigurationError,
|
||||
}),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
input.setStore("status", "partial")
|
||||
return
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||
import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@kilocode/sdk/v2/client"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { State } from "./types"
|
||||
import { applyDirectoryEvent, applyGlobalEvent } from "./event-reducer"
|
||||
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./event-reducer"
|
||||
|
||||
const rootSession = (input: { id: string; parentID?: string; archived?: number }) =>
|
||||
({
|
||||
@@ -248,6 +248,62 @@ describe("applyDirectoryEvent", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("cleans caches for trimmed sessions on session.created", () => {
|
||||
const dropped = rootSession({ id: "ses_b" })
|
||||
const kept = rootSession({ id: "ses_a" })
|
||||
const message = userMessage("msg_1", dropped.id)
|
||||
const todos: string[] = []
|
||||
const [store, setStore] = createStore(
|
||||
baseState({
|
||||
limit: 1,
|
||||
session: [dropped],
|
||||
message: { [dropped.id]: [message] },
|
||||
part: { [message.id]: [textPart("prt_1", dropped.id, message.id)] },
|
||||
session_diff: { [dropped.id]: [] },
|
||||
todo: { [dropped.id]: [] },
|
||||
permission: { [dropped.id]: [] },
|
||||
question: { [dropped.id]: [] },
|
||||
session_status: { [dropped.id]: { type: "busy" } },
|
||||
}),
|
||||
)
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: { type: "session.created", properties: { info: kept } },
|
||||
store,
|
||||
setStore,
|
||||
push() {},
|
||||
directory: "/tmp",
|
||||
loadLsp() {},
|
||||
setSessionTodo(sessionID, value) {
|
||||
if (value !== undefined) return
|
||||
todos.push(sessionID)
|
||||
},
|
||||
})
|
||||
|
||||
expect(store.session.map((x) => x.id)).toEqual([kept.id])
|
||||
expect(store.message[dropped.id]).toBeUndefined()
|
||||
expect(store.part[message.id]).toBeUndefined()
|
||||
expect(store.session_diff[dropped.id]).toBeUndefined()
|
||||
expect(store.todo[dropped.id]).toBeUndefined()
|
||||
expect(store.permission[dropped.id]).toBeUndefined()
|
||||
expect(store.question[dropped.id]).toBeUndefined()
|
||||
expect(store.session_status[dropped.id]).toBeUndefined()
|
||||
expect(todos).toEqual([dropped.id])
|
||||
})
|
||||
|
||||
test("cleanupDroppedSessionCaches clears part-only orphan state", () => {
|
||||
const [store, setStore] = createStore(
|
||||
baseState({
|
||||
session: [rootSession({ id: "ses_keep" })],
|
||||
part: { msg_1: [textPart("prt_1", "ses_drop", "msg_1")] },
|
||||
}),
|
||||
)
|
||||
|
||||
cleanupDroppedSessionCaches(store, setStore, store.session)
|
||||
|
||||
expect(store.part.msg_1).toBeUndefined()
|
||||
})
|
||||
|
||||
test("upserts and removes messages while clearing orphaned parts", () => {
|
||||
const sessionID = "ses_1"
|
||||
const [store, setStore] = createStore(
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
} from "@kilocode/sdk/v2/client"
|
||||
import type { State, VcsCache } from "./types"
|
||||
import { trimSessions } from "./session-trim"
|
||||
import { dropSessionCaches } from "./session-cache"
|
||||
|
||||
export function applyGlobalEvent(input: {
|
||||
event: { type: string; properties?: unknown }
|
||||
@@ -40,37 +41,44 @@ export function applyGlobalEvent(input: {
|
||||
}
|
||||
|
||||
function cleanupSessionCaches(
|
||||
store: Store<State>,
|
||||
setStore: SetStoreFunction<State>,
|
||||
sessionID: string,
|
||||
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void,
|
||||
) {
|
||||
if (!sessionID) return
|
||||
const hasAny =
|
||||
store.message[sessionID] !== undefined ||
|
||||
store.session_diff[sessionID] !== undefined ||
|
||||
store.todo[sessionID] !== undefined ||
|
||||
store.permission[sessionID] !== undefined ||
|
||||
store.question[sessionID] !== undefined ||
|
||||
store.session_status[sessionID] !== undefined
|
||||
setSessionTodo?.(sessionID, undefined)
|
||||
if (!hasAny) return
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const messages = draft.message[sessionID]
|
||||
if (messages) {
|
||||
for (const message of messages) {
|
||||
const id = message?.id
|
||||
if (!id) continue
|
||||
delete draft.part[id]
|
||||
}
|
||||
}
|
||||
delete draft.message[sessionID]
|
||||
delete draft.session_diff[sessionID]
|
||||
delete draft.todo[sessionID]
|
||||
delete draft.permission[sessionID]
|
||||
delete draft.question[sessionID]
|
||||
delete draft.session_status[sessionID]
|
||||
dropSessionCaches(draft, [sessionID])
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function cleanupDroppedSessionCaches(
|
||||
store: Store<State>,
|
||||
setStore: SetStoreFunction<State>,
|
||||
next: Session[],
|
||||
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void,
|
||||
) {
|
||||
const keep = new Set(next.map((item) => item.id))
|
||||
const stale = [
|
||||
...Object.keys(store.message),
|
||||
...Object.keys(store.session_diff),
|
||||
...Object.keys(store.todo),
|
||||
...Object.keys(store.permission),
|
||||
...Object.keys(store.question),
|
||||
...Object.keys(store.session_status),
|
||||
...Object.values(store.part)
|
||||
.map((parts) => parts?.find((part) => !!part?.sessionID)?.sessionID)
|
||||
.filter((sessionID): sessionID is string => !!sessionID),
|
||||
].filter((sessionID, index, list) => !keep.has(sessionID) && list.indexOf(sessionID) === index)
|
||||
if (stale.length === 0) return
|
||||
for (const sessionID of stale) {
|
||||
setSessionTodo?.(sessionID, undefined)
|
||||
}
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
dropSessionCaches(draft, stale)
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -102,6 +110,7 @@ export function applyDirectoryEvent(input: {
|
||||
next.splice(result.index, 0, info)
|
||||
const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission })
|
||||
input.setStore("session", reconcile(trimmed, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
|
||||
if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1)
|
||||
break
|
||||
}
|
||||
@@ -117,7 +126,7 @@ export function applyDirectoryEvent(input: {
|
||||
}),
|
||||
)
|
||||
}
|
||||
cleanupSessionCaches(input.store, input.setStore, info.id, input.setSessionTodo)
|
||||
cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo)
|
||||
if (info.parentID) break
|
||||
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||
break
|
||||
@@ -130,6 +139,7 @@ export function applyDirectoryEvent(input: {
|
||||
next.splice(result.index, 0, info)
|
||||
const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission })
|
||||
input.setStore("session", reconcile(trimmed, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
|
||||
break
|
||||
}
|
||||
case "session.deleted": {
|
||||
@@ -143,7 +153,7 @@ export function applyDirectoryEvent(input: {
|
||||
}),
|
||||
)
|
||||
}
|
||||
cleanupSessionCaches(input.store, input.setStore, info.id, input.setSessionTodo)
|
||||
cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo)
|
||||
if (info.parentID) break
|
||||
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||
break
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type {
|
||||
FileDiff,
|
||||
Message,
|
||||
Part,
|
||||
PermissionRequest,
|
||||
QuestionRequest,
|
||||
SessionStatus,
|
||||
Todo,
|
||||
} from "@kilocode/sdk/v2/client"
|
||||
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
|
||||
|
||||
const msg = (id: string, sessionID: string) =>
|
||||
({
|
||||
id,
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: 1 },
|
||||
agent: "assistant",
|
||||
model: { providerID: "openai", modelID: "gpt" },
|
||||
}) as Message
|
||||
|
||||
const part = (id: string, sessionID: string, messageID: string) =>
|
||||
({
|
||||
id,
|
||||
sessionID,
|
||||
messageID,
|
||||
type: "text",
|
||||
text: id,
|
||||
}) as Part
|
||||
|
||||
describe("app session cache", () => {
|
||||
test("dropSessionCaches clears orphaned parts without message rows", () => {
|
||||
const store: {
|
||||
session_status: Record<string, SessionStatus | undefined>
|
||||
session_diff: Record<string, FileDiff[] | undefined>
|
||||
todo: Record<string, Todo[] | undefined>
|
||||
message: Record<string, Message[] | undefined>
|
||||
part: Record<string, Part[] | undefined>
|
||||
permission: Record<string, PermissionRequest[] | undefined>
|
||||
question: Record<string, QuestionRequest[] | undefined>
|
||||
} = {
|
||||
session_status: { ses_1: { type: "busy" } as SessionStatus },
|
||||
session_diff: { ses_1: [] },
|
||||
todo: { ses_1: [] as Todo[] },
|
||||
message: {},
|
||||
part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
|
||||
permission: { ses_1: [] as PermissionRequest[] },
|
||||
question: { ses_1: [] as QuestionRequest[] },
|
||||
}
|
||||
|
||||
dropSessionCaches(store, ["ses_1"])
|
||||
|
||||
expect(store.message.ses_1).toBeUndefined()
|
||||
expect(store.part.msg_1).toBeUndefined()
|
||||
expect(store.todo.ses_1).toBeUndefined()
|
||||
expect(store.session_diff.ses_1).toBeUndefined()
|
||||
expect(store.session_status.ses_1).toBeUndefined()
|
||||
expect(store.permission.ses_1).toBeUndefined()
|
||||
expect(store.question.ses_1).toBeUndefined()
|
||||
})
|
||||
|
||||
test("dropSessionCaches clears message-backed parts", () => {
|
||||
const m = msg("msg_1", "ses_1")
|
||||
const store: {
|
||||
session_status: Record<string, SessionStatus | undefined>
|
||||
session_diff: Record<string, FileDiff[] | undefined>
|
||||
todo: Record<string, Todo[] | undefined>
|
||||
message: Record<string, Message[] | undefined>
|
||||
part: Record<string, Part[] | undefined>
|
||||
permission: Record<string, PermissionRequest[] | undefined>
|
||||
question: Record<string, QuestionRequest[] | undefined>
|
||||
} = {
|
||||
session_status: {},
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
message: { ses_1: [m] },
|
||||
part: { [m.id]: [part("prt_1", "ses_1", m.id)] },
|
||||
permission: {},
|
||||
question: {},
|
||||
}
|
||||
|
||||
dropSessionCaches(store, ["ses_1"])
|
||||
|
||||
expect(store.message.ses_1).toBeUndefined()
|
||||
expect(store.part[m.id]).toBeUndefined()
|
||||
})
|
||||
|
||||
test("pickSessionCacheEvictions preserves requested sessions", () => {
|
||||
const seen = new Set(["ses_1", "ses_2", "ses_3"])
|
||||
|
||||
const stale = pickSessionCacheEvictions({
|
||||
seen,
|
||||
keep: "ses_4",
|
||||
limit: 2,
|
||||
preserve: ["ses_1"],
|
||||
})
|
||||
|
||||
expect(stale).toEqual(["ses_2", "ses_3"])
|
||||
expect([...seen]).toEqual(["ses_1", "ses_4"])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
import type {
|
||||
FileDiff,
|
||||
Message,
|
||||
Part,
|
||||
PermissionRequest,
|
||||
QuestionRequest,
|
||||
SessionStatus,
|
||||
Todo,
|
||||
} from "@kilocode/sdk/v2/client"
|
||||
|
||||
export const SESSION_CACHE_LIMIT = 40
|
||||
|
||||
type SessionCache = {
|
||||
session_status: Record<string, SessionStatus | undefined>
|
||||
session_diff: Record<string, FileDiff[] | undefined>
|
||||
todo: Record<string, Todo[] | undefined>
|
||||
message: Record<string, Message[] | undefined>
|
||||
part: Record<string, Part[] | undefined>
|
||||
permission: Record<string, PermissionRequest[] | undefined>
|
||||
question: Record<string, QuestionRequest[] | undefined>
|
||||
}
|
||||
|
||||
export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable<string>) {
|
||||
const stale = new Set(Array.from(sessionIDs).filter(Boolean))
|
||||
if (stale.size === 0) return
|
||||
|
||||
for (const key of Object.keys(store.part)) {
|
||||
const parts = store.part[key]
|
||||
if (!parts?.some((part) => stale.has(part?.sessionID ?? ""))) continue
|
||||
delete store.part[key]
|
||||
}
|
||||
|
||||
for (const sessionID of stale) {
|
||||
delete store.message[sessionID]
|
||||
delete store.todo[sessionID]
|
||||
delete store.session_diff[sessionID]
|
||||
delete store.session_status[sessionID]
|
||||
delete store.permission[sessionID]
|
||||
delete store.question[sessionID]
|
||||
}
|
||||
}
|
||||
|
||||
export function pickSessionCacheEvictions(input: {
|
||||
seen: Set<string>
|
||||
keep: string
|
||||
limit: number
|
||||
preserve?: Iterable<string>
|
||||
}) {
|
||||
const stale: string[] = []
|
||||
const keep = new Set([input.keep, ...Array.from(input.preserve ?? [])])
|
||||
if (input.seen.has(input.keep)) input.seen.delete(input.keep)
|
||||
input.seen.add(input.keep)
|
||||
for (const id of input.seen) {
|
||||
if (input.seen.size - stale.length <= input.limit) break
|
||||
if (keep.has(id)) continue
|
||||
stale.push(id)
|
||||
}
|
||||
for (const id of stale) {
|
||||
input.seen.delete(id)
|
||||
}
|
||||
return stale
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { dict as no } from "@/i18n/no"
|
||||
import { dict as br } from "@/i18n/br"
|
||||
import { dict as th } from "@/i18n/th"
|
||||
import { dict as bs } from "@/i18n/bs"
|
||||
import { dict as nl } from "@/i18n/nl"
|
||||
import { dict as tr } from "@/i18n/tr"
|
||||
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
||||
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
|
||||
@@ -36,6 +37,7 @@ import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
|
||||
import { dict as uiBr } from "@opencode-ai/ui/i18n/br"
|
||||
import { dict as uiTh } from "@opencode-ai/ui/i18n/th"
|
||||
import { dict as uiBs } from "@opencode-ai/ui/i18n/bs"
|
||||
import { dict as uiNl } from "@opencode-ai/ui/i18n/nl"
|
||||
import { dict as uiTr } from "@opencode-ai/ui/i18n/tr"
|
||||
|
||||
// kilocode_change start
|
||||
@@ -55,6 +57,7 @@ import { dict as kiloNo } from "@kilocode/kilo-i18n/no"
|
||||
import { dict as kiloBr } from "@kilocode/kilo-i18n/br"
|
||||
import { dict as kiloTh } from "@kilocode/kilo-i18n/th"
|
||||
import { dict as kiloBs } from "@kilocode/kilo-i18n/bs"
|
||||
import { dict as kiloNl } from "@kilocode/kilo-i18n/nl"
|
||||
import { dict as kiloTr } from "@kilocode/kilo-i18n/tr"
|
||||
// kilocode_change end
|
||||
|
||||
@@ -75,6 +78,7 @@ export type Locale =
|
||||
| "br"
|
||||
| "th"
|
||||
| "bs"
|
||||
| "nl"
|
||||
| "tr"
|
||||
|
||||
type RawDictionary = typeof en & typeof uiEn & typeof kiloEn // kilocode_change
|
||||
@@ -97,6 +101,7 @@ const LOCALES: readonly Locale[] = [
|
||||
"pl",
|
||||
"ru",
|
||||
"bs",
|
||||
"nl",
|
||||
"ar",
|
||||
"no",
|
||||
"br",
|
||||
@@ -104,6 +109,28 @@ const LOCALES: readonly Locale[] = [
|
||||
"tr",
|
||||
]
|
||||
|
||||
const INTL: Record<Locale, string> = {
|
||||
en: "en",
|
||||
zh: "zh-Hans",
|
||||
zht: "zh-Hant",
|
||||
ko: "ko",
|
||||
de: "de",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
da: "da",
|
||||
ja: "ja",
|
||||
pl: "pl",
|
||||
ru: "ru",
|
||||
ar: "ar",
|
||||
no: "nb-NO",
|
||||
br: "pt-BR",
|
||||
th: "th",
|
||||
bs: "bs",
|
||||
nl: "nl",
|
||||
|
||||
tr: "tr",
|
||||
}
|
||||
|
||||
const LABEL_KEY: Record<Locale, keyof Dictionary> = {
|
||||
en: "language.en",
|
||||
zh: "language.zh",
|
||||
@@ -121,6 +148,7 @@ const LABEL_KEY: Record<Locale, keyof Dictionary> = {
|
||||
br: "language.br",
|
||||
th: "language.th",
|
||||
bs: "language.bs",
|
||||
nl: "language.nl",
|
||||
tr: "language.tr",
|
||||
}
|
||||
|
||||
@@ -142,10 +170,12 @@ const DICT: Record<Locale, Dictionary> = {
|
||||
br: { ...base, ...i18n.flatten({ ...br, ...uiBr, ...kiloBr }) },
|
||||
th: { ...base, ...i18n.flatten({ ...th, ...uiTh, ...kiloTh }) },
|
||||
bs: { ...base, ...i18n.flatten({ ...bs, ...uiBs, ...kiloBs }) },
|
||||
nl: { ...base, ...i18n.flatten({ ...nl, ...uiNl, ...kiloNl }) },
|
||||
tr: { ...base, ...i18n.flatten({ ...tr, ...uiTr, ...kiloTr }) },
|
||||
}
|
||||
|
||||
const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [
|
||||
{ locale: "en", match: (language) => language.startsWith("en") },
|
||||
{ locale: "zht", match: (language) => language.startsWith("zh") && language.includes("hant") },
|
||||
{ locale: "zh", match: (language) => language.startsWith("zh") },
|
||||
{ locale: "ko", match: (language) => language.startsWith("ko") },
|
||||
@@ -164,6 +194,7 @@ const localeMatchers: Array<{ locale: Locale; match: (language: string) => boole
|
||||
{ locale: "br", match: (language) => language.startsWith("pt") },
|
||||
{ locale: "th", match: (language) => language.startsWith("th") },
|
||||
{ locale: "bs", match: (language) => language.startsWith("bs") },
|
||||
{ locale: "nl", match: (language) => language.startsWith("nl") },
|
||||
{ locale: "tr", match: (language) => language.startsWith("tr") },
|
||||
]
|
||||
|
||||
@@ -184,6 +215,7 @@ const PARITY_CHECK: Record<Exclude<Locale, "en">, Record<ParityKey, string>> = {
|
||||
br,
|
||||
th,
|
||||
bs,
|
||||
nl,
|
||||
tr,
|
||||
}
|
||||
void PARITY_CHECK
|
||||
@@ -217,6 +249,8 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
)
|
||||
|
||||
const locale = createMemo<Locale>(() => normalizeLocale(store.locale))
|
||||
console.log("locale", locale())
|
||||
const intl = createMemo(() => INTL[locale()])
|
||||
|
||||
const dict = createMemo<Dictionary>(() => DICT[locale()])
|
||||
|
||||
@@ -233,6 +267,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
return {
|
||||
ready,
|
||||
locale,
|
||||
intl,
|
||||
locales: LOCALES,
|
||||
label,
|
||||
t,
|
||||
|
||||
@@ -35,6 +35,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
const agent = (() => {
|
||||
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
|
||||
const models = useModels()
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
current?: string
|
||||
}>({
|
||||
@@ -53,11 +55,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
setStore("current", undefined)
|
||||
return
|
||||
}
|
||||
if (name && available.some((x) => x.name === name)) {
|
||||
setStore("current", name)
|
||||
return
|
||||
}
|
||||
setStore("current", available[0].name)
|
||||
const match = name ? available.find((x) => x.name === name) : undefined
|
||||
const value = match ?? available[0]
|
||||
if (!value) return
|
||||
setStore("current", value.name)
|
||||
if (!value.model) return
|
||||
setModel({
|
||||
providerID: value.model.providerID,
|
||||
modelID: value.model.modelID,
|
||||
})
|
||||
if (value.variant)
|
||||
models.variant.set({ providerID: value.model.providerID, modelID: value.model.modelID }, value.variant)
|
||||
},
|
||||
move(direction: 1 | -1) {
|
||||
const available = list()
|
||||
@@ -71,11 +79,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const value = available[next]
|
||||
if (!value) return
|
||||
setStore("current", value.name)
|
||||
if (value.model)
|
||||
setModel({
|
||||
providerID: value.model.providerID,
|
||||
modelID: value.model.modelID,
|
||||
})
|
||||
if (!value.model) return
|
||||
setModel({
|
||||
providerID: value.model.providerID,
|
||||
modelID: value.model.modelID,
|
||||
})
|
||||
if (value.variant)
|
||||
models.variant.set({ providerID: value.model.providerID, modelID: value.model.modelID }, value.variant)
|
||||
},
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { PermissionRequest, Session } from "@kilocode/sdk/v2/client"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { autoRespondsPermission } from "./permission-auto-respond"
|
||||
import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond"
|
||||
|
||||
const session = (input: { id: string; parentID?: string }) =>
|
||||
({
|
||||
@@ -60,4 +60,43 @@ describe("autoRespondsPermission", () => {
|
||||
|
||||
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true)
|
||||
})
|
||||
|
||||
test("falls back to directory-level auto-accept", () => {
|
||||
const directory = "/tmp/project"
|
||||
const sessions = [session({ id: "root" })]
|
||||
const autoAccept = {
|
||||
[`${base64Encode(directory)}/*`]: true,
|
||||
}
|
||||
|
||||
expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(true)
|
||||
})
|
||||
|
||||
test("session-level override takes precedence over directory-level", () => {
|
||||
const directory = "/tmp/project"
|
||||
const sessions = [session({ id: "root" })]
|
||||
const autoAccept = {
|
||||
[`${base64Encode(directory)}/*`]: true,
|
||||
[`${base64Encode(directory)}/root`]: false,
|
||||
}
|
||||
|
||||
expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isDirectoryAutoAccepting", () => {
|
||||
test("returns true when directory key is set", () => {
|
||||
const directory = "/tmp/project"
|
||||
const autoAccept = { [`${base64Encode(directory)}/*`]: true }
|
||||
expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false when directory key is not set", () => {
|
||||
expect(isDirectoryAutoAccepting({}, "/tmp/project")).toBe(false)
|
||||
})
|
||||
|
||||
test("returns false when directory key is explicitly false", () => {
|
||||
const directory = "/tmp/project"
|
||||
const autoAccept = { [`${base64Encode(directory)}/*`]: false }
|
||||
expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,9 +5,19 @@ export function acceptKey(sessionID: string, directory?: string) {
|
||||
return `${base64Encode(directory)}/${sessionID}`
|
||||
}
|
||||
|
||||
export function directoryAcceptKey(directory: string) {
|
||||
return `${base64Encode(directory)}/*`
|
||||
}
|
||||
|
||||
function accepted(autoAccept: Record<string, boolean>, sessionID: string, directory?: string) {
|
||||
const key = acceptKey(sessionID, directory)
|
||||
return autoAccept[key] ?? autoAccept[sessionID]
|
||||
const directoryKey = directory ? directoryAcceptKey(directory) : undefined
|
||||
return autoAccept[key] ?? autoAccept[sessionID] ?? (directoryKey ? autoAccept[directoryKey] : undefined)
|
||||
}
|
||||
|
||||
export function isDirectoryAutoAccepting(autoAccept: Record<string, boolean>, directory: string) {
|
||||
const key = directoryAcceptKey(directory)
|
||||
return autoAccept[key] ?? false
|
||||
}
|
||||
|
||||
function sessionLineage(session: { id: string; parentID?: string }[], sessionID: string) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createMemo, onCleanup } from "solid-js"
|
||||
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import type { PermissionRequest } from "@kilocode/sdk/v2/client"
|
||||
@@ -7,7 +7,12 @@ import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { acceptKey, autoRespondsPermission } from "./permission-auto-respond"
|
||||
import {
|
||||
acceptKey,
|
||||
directoryAcceptKey,
|
||||
isDirectoryAutoAccepting,
|
||||
autoRespondsPermission,
|
||||
} from "./permission-auto-respond"
|
||||
|
||||
type PermissionRespondFn = (input: {
|
||||
sessionID: string
|
||||
@@ -76,6 +81,25 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
}),
|
||||
)
|
||||
|
||||
// When config has permission: "allow", auto-enable directory-level auto-accept
|
||||
createEffect(() => {
|
||||
if (!ready()) return
|
||||
const directory = decode64(params.dir)
|
||||
if (!directory) return
|
||||
const [childStore] = globalSync.child(directory)
|
||||
const perm = childStore.config.permission
|
||||
if (typeof perm === "string" && perm === "allow") {
|
||||
const key = directoryAcceptKey(directory)
|
||||
if (store.autoAccept[key] === undefined) {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.autoAccept[key] = true
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const MAX_RESPONDED = 1000
|
||||
const RESPONDED_TTL_MS = 60 * 60 * 1000
|
||||
const responded = new Map<string, number>()
|
||||
@@ -119,6 +143,10 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory)
|
||||
}
|
||||
|
||||
function isAutoAcceptingDirectory(directory: string) {
|
||||
return isDirectoryAutoAccepting(store.autoAccept, directory)
|
||||
}
|
||||
|
||||
function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
|
||||
const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
|
||||
return autoRespondsPermission(store.autoAccept, session, permission, directory)
|
||||
@@ -142,6 +170,36 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
})
|
||||
onCleanup(unsubscribe)
|
||||
|
||||
function enableDirectory(directory: string) {
|
||||
const key = directoryAcceptKey(directory)
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.autoAccept[key] = true
|
||||
}),
|
||||
)
|
||||
|
||||
globalSDK.client.permission
|
||||
.list({ directory })
|
||||
.then((x) => {
|
||||
if (!isAutoAcceptingDirectory(directory)) return
|
||||
for (const perm of x.data ?? []) {
|
||||
if (!perm?.id) continue
|
||||
if (!shouldAutoRespond(perm, directory)) continue
|
||||
respondOnce(perm, directory)
|
||||
}
|
||||
})
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
function disableDirectory(directory: string) {
|
||||
const key = directoryAcceptKey(directory)
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.autoAccept[key] = false
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function enable(sessionID: string, directory: string) {
|
||||
const key = acceptKey(sessionID, directory)
|
||||
const version = bumpEnableVersion(sessionID, directory)
|
||||
@@ -185,6 +243,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
return shouldAutoRespond(permission, directory)
|
||||
},
|
||||
isAutoAccepting,
|
||||
isAutoAcceptingDirectory,
|
||||
toggleAutoAccept(sessionID: string, directory: string) {
|
||||
if (isAutoAccepting(sessionID, directory)) {
|
||||
disable(sessionID, directory)
|
||||
@@ -193,6 +252,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
|
||||
enable(sessionID, directory)
|
||||
},
|
||||
toggleAutoAcceptDirectory(directory: string) {
|
||||
if (isAutoAcceptingDirectory(directory)) {
|
||||
disableDirectory(directory)
|
||||
return
|
||||
}
|
||||
enableDirectory(directory)
|
||||
},
|
||||
enableAutoAccept(sessionID: string, directory: string) {
|
||||
if (isAutoAccepting(sessionID, directory)) return
|
||||
enable(sessionID, directory)
|
||||
@@ -201,6 +267,11 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
disable(sessionID, directory)
|
||||
},
|
||||
permissionsEnabled,
|
||||
isPermissionAllowAll(directory: string) {
|
||||
const [childStore] = globalSync.child(directory)
|
||||
const perm = childStore.config.permission
|
||||
return typeof perm === "string" && perm === "allow"
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useSDK } from "./sdk"
|
||||
import type { Message, Part } from "@kilocode/sdk/v2/client"
|
||||
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
||||
|
||||
function sortParts(parts: Part[]) {
|
||||
return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))
|
||||
@@ -108,6 +109,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const inflightDiff = new Map<string, Promise<void>>()
|
||||
const inflightTodo = new Map<string, Promise<void>>()
|
||||
const maxDirs = 30
|
||||
const seen = new Map<string, Set<string>>()
|
||||
const [meta, setMeta] = createStore({
|
||||
limit: {} as Record<string, number>,
|
||||
complete: {} as Record<string, boolean>,
|
||||
@@ -121,6 +124,62 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
return undefined
|
||||
}
|
||||
|
||||
const seenFor = (directory: string) => {
|
||||
const existing = seen.get(directory)
|
||||
if (existing) {
|
||||
seen.delete(directory)
|
||||
seen.set(directory, existing)
|
||||
return existing
|
||||
}
|
||||
const created = new Set<string>()
|
||||
seen.set(directory, created)
|
||||
while (seen.size > maxDirs) {
|
||||
const first = seen.keys().next().value
|
||||
if (!first) break
|
||||
const stale = [...(seen.get(first) ?? [])]
|
||||
seen.delete(first)
|
||||
const [, setStore] = globalSync.child(first, { bootstrap: false })
|
||||
evict(first, setStore, stale)
|
||||
}
|
||||
return created
|
||||
}
|
||||
|
||||
const clearMeta = (directory: string, sessionIDs: string[]) => {
|
||||
if (sessionIDs.length === 0) return
|
||||
setMeta(
|
||||
produce((draft) => {
|
||||
for (const sessionID of sessionIDs) {
|
||||
const key = keyFor(directory, sessionID)
|
||||
delete draft.limit[key]
|
||||
delete draft.complete[key]
|
||||
delete draft.loading[key]
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const evict = (directory: string, setStore: Setter, sessionIDs: string[]) => {
|
||||
if (sessionIDs.length === 0) return
|
||||
for (const sessionID of sessionIDs) {
|
||||
globalSync.todo.set(sessionID, undefined)
|
||||
}
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
dropSessionCaches(draft, sessionIDs)
|
||||
}),
|
||||
)
|
||||
clearMeta(directory, sessionIDs)
|
||||
}
|
||||
|
||||
const touch = (directory: string, setStore: Setter, sessionID: string) => {
|
||||
const stale = pickSessionCacheEvictions({
|
||||
seen: seenFor(directory),
|
||||
keep: sessionID,
|
||||
limit: SESSION_CACHE_LIMIT,
|
||||
})
|
||||
evict(directory, setStore, stale)
|
||||
}
|
||||
|
||||
const fetchMessages = async (input: { client: typeof sdk.client; sessionID: string; limit: number }) => {
|
||||
const messages = await retry(() =>
|
||||
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }),
|
||||
@@ -135,6 +194,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
}
|
||||
}
|
||||
|
||||
const tracked = (directory: string, sessionID: string) => seen.get(directory)?.has(sessionID) ?? false
|
||||
|
||||
const loadMessages = async (input: {
|
||||
directory: string
|
||||
client: typeof sdk.client
|
||||
@@ -148,6 +209,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
setMeta("loading", key, true)
|
||||
await fetchMessages(input)
|
||||
.then((next) => {
|
||||
if (!tracked(input.directory, input.sessionID)) return
|
||||
batch(() => {
|
||||
input.setStore("message", input.sessionID, reconcile(next.session, { key: "id" }))
|
||||
for (const p of next.part) {
|
||||
@@ -158,6 +220,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
if (!tracked(input.directory, input.sessionID)) return
|
||||
setMeta("loading", key, false)
|
||||
})
|
||||
}
|
||||
@@ -199,6 +262,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
parts: Part[]
|
||||
agent: string
|
||||
model: { providerID: string; modelID: string }
|
||||
variant?: string
|
||||
}) {
|
||||
const message: Message = {
|
||||
id: input.messageID,
|
||||
@@ -207,6 +271,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
time: { created: Date.now() },
|
||||
agent: input.agent,
|
||||
model: input.model,
|
||||
variant: input.variant,
|
||||
}
|
||||
const [, setStore] = target()
|
||||
setOptimisticAdd(setStore as (...args: unknown[]) => void, {
|
||||
@@ -222,11 +287,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const key = keyFor(directory, sessionID)
|
||||
const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
|
||||
|
||||
touch(directory, setStore, sessionID)
|
||||
|
||||
if (store.message[sessionID] !== undefined && hasSession && meta.limit[key] !== undefined) return
|
||||
|
||||
const limit = meta.limit[key] ?? messagePageSize
|
||||
|
||||
const sessionReq = hasSession
|
||||
? Promise.resolve()
|
||||
: retry(() => client.session.get({ sessionID })).then((session) => {
|
||||
if (!tracked(directory, sessionID)) return
|
||||
const data = session.data
|
||||
if (!data) return
|
||||
setStore(
|
||||
@@ -256,11 +326,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
touch(directory, setStore, sessionID)
|
||||
if (store.session_diff[sessionID] !== undefined) return
|
||||
|
||||
const key = keyFor(directory, sessionID)
|
||||
return runInflight(inflightDiff, key, () =>
|
||||
retry(() => client.session.diff({ sessionID })).then((diff) => {
|
||||
if (!tracked(directory, sessionID)) return
|
||||
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
|
||||
}),
|
||||
)
|
||||
@@ -269,6 +341,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
touch(directory, setStore, sessionID)
|
||||
const existing = store.todo[sessionID]
|
||||
const cached = globalSync.data.session_todo[sessionID]
|
||||
if (existing !== undefined) {
|
||||
@@ -285,6 +358,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const key = keyFor(directory, sessionID)
|
||||
return runInflight(inflightTodo, key, () =>
|
||||
retry(() => client.session.todo({ sessionID })).then((todo) => {
|
||||
if (!tracked(directory, sessionID)) return
|
||||
const list = todo.data ?? []
|
||||
setStore("todo", sessionID, reconcile(list, { key: "id" }))
|
||||
globalSync.todo.set(sessionID, list)
|
||||
@@ -308,6 +382,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
touch(directory, setStore, sessionID)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const step = count ?? messagePageSize
|
||||
if (meta.loading[key]) return
|
||||
@@ -323,6 +398,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
})
|
||||
},
|
||||
},
|
||||
evict(sessionID: string, directory = sdk.directory) {
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
seenFor(directory).delete(sessionID)
|
||||
evict(directory, setStore, [sessionID])
|
||||
},
|
||||
fetch: async (count = 10) => {
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js"
|
||||
import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "solid-js"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useSDK } from "./sdk"
|
||||
import type { Platform } from "./platform"
|
||||
@@ -38,6 +38,16 @@ type TerminalCacheEntry = {
|
||||
|
||||
const caches = new Set<Map<string, TerminalCacheEntry>>()
|
||||
|
||||
const trimTerminal = (pty: LocalPTY) => {
|
||||
if (!pty.buffer && pty.cursor === undefined && pty.scrollY === undefined) return pty
|
||||
return {
|
||||
...pty,
|
||||
buffer: undefined,
|
||||
cursor: undefined,
|
||||
scrollY: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform) {
|
||||
const key = getWorkspaceTerminalCacheKey(dir)
|
||||
for (const cache of caches) {
|
||||
@@ -188,6 +198,18 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
||||
console.error("Failed to update terminal", error)
|
||||
})
|
||||
},
|
||||
trim(id: string) {
|
||||
const index = store.all.findIndex((x) => x.id === id)
|
||||
if (index === -1) return
|
||||
setStore("all", index, (pty) => trimTerminal(pty))
|
||||
},
|
||||
trimAll() {
|
||||
setStore("all", (all) => {
|
||||
const next = all.map(trimTerminal)
|
||||
if (next.every((pty, index) => pty === all[index])) return all
|
||||
return next
|
||||
})
|
||||
},
|
||||
async clone(id: string) {
|
||||
const index = store.all.findIndex((x) => x.id === id)
|
||||
const pty = store.all[index]
|
||||
@@ -322,12 +344,27 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||
|
||||
const workspace = createMemo(() => loadWorkspace(params.dir!, params.id))
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => ({ dir: params.dir, id: params.id }),
|
||||
(next, prev) => {
|
||||
if (!prev?.dir) return
|
||||
if (next.dir === prev.dir && next.id === prev.id) return
|
||||
if (next.dir === prev.dir && next.id) return
|
||||
loadWorkspace(prev.dir, prev.id).trimAll()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
ready: () => workspace().ready(),
|
||||
all: () => workspace().all(),
|
||||
active: () => workspace().active(),
|
||||
new: () => workspace().new(),
|
||||
update: (pty: Partial<LocalPTY> & { id: string }) => workspace().update(pty),
|
||||
trim: (id: string) => workspace().trim(id),
|
||||
trimAll: () => workspace().trimAll(),
|
||||
clone: (id: string) => workspace().clone(id),
|
||||
open: (id: string) => workspace().open(id),
|
||||
close: (id: string) => workspace().close(id),
|
||||
|
||||
Generated
+1
@@ -455,6 +455,7 @@ export const dict = {
|
||||
"session.todo.title": "المهام",
|
||||
"session.todo.collapse": "طي",
|
||||
"session.todo.expand": "توسيع",
|
||||
"session.new.title": "ابنِ أي شيء",
|
||||
"session.new.worktree.main": "الفرع الرئيسي",
|
||||
"session.new.worktree.mainWithBranch": "الفرع الرئيسي ({{branch}})",
|
||||
"session.new.worktree.create": "إنشاء شجرة عمل جديدة",
|
||||
|
||||
Generated
+1
@@ -459,6 +459,7 @@ export const dict = {
|
||||
"session.todo.title": "Tarefas",
|
||||
"session.todo.collapse": "Recolher",
|
||||
"session.todo.expand": "Expandir",
|
||||
"session.new.title": "Crie qualquer coisa",
|
||||
"session.new.worktree.main": "Branch principal",
|
||||
"session.new.worktree.mainWithBranch": "Branch principal ({{branch}})",
|
||||
"session.new.worktree.create": "Criar novo worktree",
|
||||
|
||||
Generated
+1
-9
@@ -113,7 +113,6 @@ export const dict = {
|
||||
"dialog.model.empty": "Nema rezultata za modele",
|
||||
"dialog.model.manage": "Upravljaj modelima",
|
||||
"dialog.model.manage.description": "Prilagodi koji se modeli prikazuju u izborniku modela.",
|
||||
"dialog.model.manage.provider.toggle": "Uključi/isključi sve {{provider}} modele",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Besplatni modeli koje obezbjeđuje Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Dodaj još modela od popularnih provajdera",
|
||||
@@ -500,7 +499,6 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Učitavanje izmjena...",
|
||||
"session.review.empty": "Još nema izmjena u ovoj sesiji",
|
||||
"session.review.noChanges": "Nema izmjena",
|
||||
"session.review.noVcs": "Nije otkriven git VCS, stoga promjene sesije neće biti detektovane",
|
||||
|
||||
"session.files.selectToOpen": "Odaberi datoteku za otvaranje",
|
||||
"session.files.all": "Sve datoteke",
|
||||
@@ -517,11 +515,7 @@ export const dict = {
|
||||
"session.todo.collapse": "Sažmi",
|
||||
"session.todo.expand": "Proširi",
|
||||
|
||||
"session.modeSwitch.switching": "Prebacivanje u {{mode}} način rada…",
|
||||
"session.modeSwitch.waiting": "Čekanje na završetak trenutnog zadatka",
|
||||
"session.modeSwitch.notAvailable": "Agent nije dostupan",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" nije pronađen, koristi se "{{actual}}"',
|
||||
|
||||
"session.new.title": "Napravi bilo šta",
|
||||
"session.new.worktree.main": "Glavna grana",
|
||||
"session.new.worktree.mainWithBranch": "Glavna grana ({{branch}})",
|
||||
"session.new.worktree.create": "Kreiraj novi worktree",
|
||||
@@ -619,8 +613,6 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Prilagodi temu Kilo-a.",
|
||||
"settings.general.row.font.title": "Font",
|
||||
"settings.general.row.font.description": "Prilagodi monospace font koji se koristi u blokovima koda",
|
||||
"settings.general.row.reasoningSummaries.title": "Prikaži sažetke razmišljanja",
|
||||
"settings.general.row.reasoningSummaries.description": "Prikaži sažetke razmišljanja modela u vremenskoj liniji",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Proširi dijelove shell alata",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
|
||||
Generated
+1
-9
@@ -113,7 +113,6 @@ export const dict = {
|
||||
"dialog.model.empty": "Ingen modeller fundet",
|
||||
"dialog.model.manage": "Administrer modeller",
|
||||
"dialog.model.manage.description": "Tilpas hvilke modeller der vises i modelvælgeren.",
|
||||
"dialog.model.manage.provider.toggle": "Skift alle {{provider}}-modeller",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere",
|
||||
@@ -497,7 +496,6 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Indlæser ændringer...",
|
||||
"session.review.empty": "Ingen ændringer i denne session endnu",
|
||||
"session.review.noChanges": "Ingen ændringer",
|
||||
"session.review.noVcs": "Ingen git VCS registreret, så sessionsændringer vil ikke blive registreret",
|
||||
"session.files.selectToOpen": "Vælg en fil at åbne",
|
||||
"session.files.all": "Alle filer",
|
||||
"session.files.binaryContent": "Binær fil (indhold kan ikke vises)",
|
||||
@@ -512,11 +510,7 @@ export const dict = {
|
||||
"session.todo.collapse": "Skjul",
|
||||
"session.todo.expand": "Udvid",
|
||||
|
||||
"session.modeSwitch.switching": "Skifter til {{mode}}-tilstand…",
|
||||
"session.modeSwitch.waiting": "Venter på at den aktuelle opgave er fuldført",
|
||||
"session.modeSwitch.notAvailable": "Agent ikke tilgængelig",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" ikke fundet, bruger "{{actual}}"',
|
||||
|
||||
"session.new.title": "Byg hvad som helst",
|
||||
"session.new.worktree.main": "Hovedgren",
|
||||
"session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})",
|
||||
"session.new.worktree.create": "Opret nyt worktree",
|
||||
@@ -614,8 +608,6 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Tilpas hvordan Kilo er temabestemt.",
|
||||
"settings.general.row.font.title": "Skrifttype",
|
||||
"settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke",
|
||||
"settings.general.row.reasoningSummaries.title": "Vis ræsonneringssammendrag",
|
||||
"settings.general.row.reasoningSummaries.description": "Vis modelræsonneringssammendrag i tidslinjen",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Udvid shell-værktøjsdele",
|
||||
"settings.general.row.shellToolPartsExpanded.description": "Vis shell-værktøjsdele udvidet som standard i tidslinjen",
|
||||
|
||||
Generated
+1
@@ -467,6 +467,7 @@ export const dict = {
|
||||
"session.todo.title": "Aufgaben",
|
||||
"session.todo.collapse": "Einklappen",
|
||||
"session.todo.expand": "Ausklappen",
|
||||
"session.new.title": "Baue, was du willst",
|
||||
"session.new.worktree.main": "Haupt-Branch",
|
||||
"session.new.worktree.mainWithBranch": "Haupt-Branch ({{branch}})",
|
||||
"session.new.worktree.create": "Neuen Worktree erstellen",
|
||||
|
||||
@@ -402,6 +402,7 @@ export const dict = {
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.bs": "Bosanski",
|
||||
"language.nl": "Nederlands",
|
||||
"language.th": "ไทย",
|
||||
"language.tr": "Türkçe",
|
||||
|
||||
@@ -511,11 +512,13 @@ export const dict = {
|
||||
"session.review.change.other": "Changes",
|
||||
"session.review.loadingChanges": "Loading changes...",
|
||||
"session.review.empty": "No changes in this session yet",
|
||||
"session.review.noVcs": "No git VCS detected, so session changes will not be detected",
|
||||
"session.review.noVcs": "No Git Version Control System detected, changes not displayed",
|
||||
"session.review.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable",
|
||||
"session.review.noChanges": "No changes",
|
||||
|
||||
"session.files.selectToOpen": "Select a file to open",
|
||||
"session.files.all": "All files",
|
||||
"session.files.empty": "No files",
|
||||
"session.files.binaryContent": "Binary file (content cannot be displayed)",
|
||||
|
||||
"session.messages.renderEarlier": "Render earlier messages",
|
||||
@@ -529,11 +532,7 @@ export const dict = {
|
||||
"session.todo.collapse": "Collapse",
|
||||
"session.todo.expand": "Expand",
|
||||
|
||||
"session.modeSwitch.switching": "Switching to {{mode}} mode…",
|
||||
"session.modeSwitch.waiting": "Waiting for current task to complete",
|
||||
"session.modeSwitch.notAvailable": "Agent not available",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" not found, using "{{actual}}"',
|
||||
|
||||
"session.new.title": "Build anything",
|
||||
"session.new.worktree.main": "Main branch",
|
||||
"session.new.worktree.mainWithBranch": "Main branch ({{branch}})",
|
||||
"session.new.worktree.create": "Create new worktree",
|
||||
|
||||
Generated
+1
-10
@@ -113,7 +113,6 @@ export const dict = {
|
||||
"dialog.model.empty": "Sin resultados de modelos",
|
||||
"dialog.model.manage": "Gestionar modelos",
|
||||
"dialog.model.manage.description": "Personalizar qué modelos aparecen en el selector de modelos.",
|
||||
"dialog.model.manage.provider.toggle": "Alternar todos los modelos de {{provider}}",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares",
|
||||
@@ -501,7 +500,6 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Cargando cambios...",
|
||||
"session.review.empty": "No hay cambios en esta sesión aún",
|
||||
"session.review.noChanges": "Sin cambios",
|
||||
"session.review.noVcs": "No se detectó VCS de git, por lo que los cambios de sesión no se detectarán",
|
||||
|
||||
"session.files.selectToOpen": "Selecciona un archivo para abrir",
|
||||
"session.files.all": "Todos los archivos",
|
||||
@@ -518,11 +516,7 @@ export const dict = {
|
||||
"session.todo.collapse": "Contraer",
|
||||
"session.todo.expand": "Expandir",
|
||||
|
||||
"session.modeSwitch.switching": "Cambiando al modo {{mode}}…",
|
||||
"session.modeSwitch.waiting": "Esperando que la tarea actual se complete",
|
||||
"session.modeSwitch.notAvailable": "Agente no disponible",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" no encontrado, usando "{{actual}}"',
|
||||
|
||||
"session.new.title": "Construye lo que quieras",
|
||||
"session.new.worktree.main": "Rama principal",
|
||||
"session.new.worktree.mainWithBranch": "Rama principal ({{branch}})",
|
||||
"session.new.worktree.create": "Crear nuevo árbol de trabajo",
|
||||
@@ -622,9 +616,6 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Personaliza el tema de Kilo.",
|
||||
"settings.general.row.font.title": "Fuente",
|
||||
"settings.general.row.font.description": "Personaliza la fuente monoespaciada usada en bloques de código",
|
||||
"settings.general.row.reasoningSummaries.title": "Mostrar resúmenes de razonamiento",
|
||||
"settings.general.row.reasoningSummaries.description":
|
||||
"Mostrar resúmenes de razonamiento del modelo en la línea de tiempo",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Expandir partes de la herramienta shell",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
|
||||
Generated
+1
-3
@@ -462,6 +462,7 @@ export const dict = {
|
||||
"session.todo.title": "Tâches",
|
||||
"session.todo.collapse": "Réduire",
|
||||
"session.todo.expand": "Développer",
|
||||
"session.new.title": "Créez ce que vous voulez",
|
||||
"session.new.worktree.main": "Branche principale",
|
||||
"session.new.worktree.mainWithBranch": "Branche principale ({{branch}})",
|
||||
"session.new.worktree.create": "Créer un nouvel arbre de travail",
|
||||
@@ -551,9 +552,6 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Personnaliser le thème d'Kilo.",
|
||||
"settings.general.row.font.title": "Police",
|
||||
"settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code",
|
||||
"settings.general.row.reasoningSummaries.title": "Afficher les résumés de raisonnement",
|
||||
"settings.general.row.reasoningSummaries.description":
|
||||
"Afficher les résumés de raisonnement du modèle dans la chronologie",
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Développer les parties de l'outil shell",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
"Afficher les parties de l'outil shell développées par défaut dans la chronologie",
|
||||
|
||||
Generated
+1
@@ -457,6 +457,7 @@ export const dict = {
|
||||
"session.todo.title": "ToDo",
|
||||
"session.todo.collapse": "折りたたむ",
|
||||
"session.todo.expand": "展開",
|
||||
"session.new.title": "何でも作る",
|
||||
"session.new.worktree.main": "メインブランチ",
|
||||
"session.new.worktree.mainWithBranch": "メインブランチ ({{branch}})",
|
||||
"session.new.worktree.create": "新しいワークツリーを作成",
|
||||
|
||||
Generated
+1
@@ -459,6 +459,7 @@ export const dict = {
|
||||
"session.todo.title": "할 일",
|
||||
"session.todo.collapse": "접기",
|
||||
"session.todo.expand": "펼치기",
|
||||
"session.new.title": "무엇이든 만들기",
|
||||
"session.new.worktree.main": "메인 브랜치",
|
||||
"session.new.worktree.mainWithBranch": "메인 브랜치 ({{branch}})",
|
||||
"session.new.worktree.create": "새 작업 트리 생성",
|
||||
|
||||
Generated
+855
@@ -0,0 +1,855 @@
|
||||
export const dict = {
|
||||
"command.category.suggested": "Voorgesteld",
|
||||
"command.category.view": "Weergave",
|
||||
"command.category.project": "Project",
|
||||
"command.category.provider": "Provider",
|
||||
"command.category.server": "Server",
|
||||
"command.category.session": "Sessie",
|
||||
"command.category.theme": "Thema",
|
||||
"command.category.language": "Taal",
|
||||
"command.category.file": "Bestand",
|
||||
"command.category.context": "Context",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Model",
|
||||
"command.category.mcp": "MCP",
|
||||
"command.category.agent": "Agent",
|
||||
"command.category.permissions": "Machtigingen",
|
||||
"command.category.workspace": "Werkruimte",
|
||||
"command.category.settings": "Instellingen",
|
||||
|
||||
"theme.scheme.system": "Systeem",
|
||||
"theme.scheme.light": "Licht",
|
||||
"theme.scheme.dark": "Donker",
|
||||
|
||||
"command.sidebar.toggle": "Zijbalk in-/uitschakelen",
|
||||
"command.project.open": "Project openen",
|
||||
"command.provider.connect": "Provider verbinden",
|
||||
"command.server.switch": "Server wisselen",
|
||||
"command.settings.open": "Instellingen openen",
|
||||
"command.session.previous": "Vorige sessie",
|
||||
"command.session.next": "Volgende sessie",
|
||||
"command.session.previous.unseen": "Vorige ongelezen sessie",
|
||||
"command.session.next.unseen": "Volgende ongelezen sessie",
|
||||
"command.session.archive": "Sessie archiveren",
|
||||
|
||||
"command.palette": "Opdrachtenpalet",
|
||||
|
||||
"command.theme.cycle": "Volgend thema",
|
||||
"command.theme.set": "Gebruik thema: {{theme}}",
|
||||
"command.theme.scheme.cycle": "Volgend kleurenschema",
|
||||
"command.theme.scheme.set": "Gebruik kleurenschema: {{scheme}}",
|
||||
|
||||
"command.language.cycle": "Volgende taal",
|
||||
"command.language.set": "Gebruik taal: {{language}}",
|
||||
|
||||
"command.session.new": "Nieuwe sessie",
|
||||
"command.file.open": "Bestand openen",
|
||||
"command.tab.close": "Tabblad sluiten",
|
||||
"command.context.addSelection": "Selectie toevoegen aan context",
|
||||
"command.context.addSelection.description": "Geselecteerde regels van het huidige bestand toevoegen",
|
||||
"command.input.focus": "Invoer focussen",
|
||||
"command.terminal.toggle": "Terminal in-/uitschakelen",
|
||||
"command.fileTree.toggle": "Bestandsboom in-/uitschakelen",
|
||||
"command.review.toggle": "Review in-/uitschakelen",
|
||||
"command.terminal.new": "Nieuwe terminal",
|
||||
"command.terminal.new.description": "Een nieuw terminal-tabblad maken",
|
||||
"command.steps.toggle": "Stappen in-/uitschakelen",
|
||||
"command.steps.toggle.description": "Stappen voor het huidige bericht tonen of verbergen",
|
||||
"command.message.previous": "Vorig bericht",
|
||||
"command.message.previous.description": "Ga naar het vorige gebruikersbericht",
|
||||
"command.message.next": "Volgend bericht",
|
||||
"command.message.next.description": "Ga naar het volgende gebruikersbericht",
|
||||
"command.model.choose": "Model kiezen",
|
||||
"command.model.choose.description": "Selecteer een ander model",
|
||||
"command.mcp.toggle": "MCPs in-/uitschakelen",
|
||||
"command.mcp.toggle.description": "MCPs in-/uitschakelen",
|
||||
"command.agent.cycle": "Volgende agent",
|
||||
"command.agent.cycle.description": "Overschakelen naar de volgende agent",
|
||||
"command.agent.cycle.reverse": "Vorige agent",
|
||||
"command.agent.cycle.reverse.description": "Overschakelen naar de vorige agent",
|
||||
"command.model.variant.cycle": "Volgende denkinspanning",
|
||||
"command.model.variant.cycle.description": "Overschakelen naar het volgende inspanningsniveau",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Machtigingen automatisch accepteren",
|
||||
"command.permissions.autoaccept.disable": "Machtigingen niet langer automatisch accepteren",
|
||||
"command.workspace.toggle": "Werkruimtes in-/uitschakelen",
|
||||
"command.workspace.toggle.description": "Meerdere werkruimtes in de zijbalk in- of uitschakelen",
|
||||
"command.session.undo": "Ongedaan maken",
|
||||
"command.session.undo.description": "Laatste bericht ongedaan maken",
|
||||
"command.session.redo": "Opnieuw uitvoeren",
|
||||
"command.session.redo.description": "Het laatst ongedaan gemaakte bericht opnieuw uitvoeren",
|
||||
"command.session.compact": "Sessie comprimeren",
|
||||
"command.session.compact.description": "Sessie samenvatten om contextgrootte te verkleinen",
|
||||
"command.session.fork": "Afsplitsen van bericht",
|
||||
"command.session.fork.description": "Nieuwe sessie maken van een eerder bericht",
|
||||
"command.session.share": "Sessie delen",
|
||||
"command.session.share.description": "Deze sessie delen en de URL naar klembord kopiëren",
|
||||
"command.session.unshare": "Sessie niet meer delen",
|
||||
"command.session.unshare.description": "Stoppen met het delen van deze sessie",
|
||||
|
||||
"palette.search.placeholder": "Zoek bestanden, opdrachten en sessies",
|
||||
"palette.empty": "Geen resultaten gevonden",
|
||||
"palette.group.commands": "Opdrachten",
|
||||
"palette.group.files": "Bestanden",
|
||||
|
||||
"dialog.provider.search.placeholder": "Zoek providers",
|
||||
"dialog.provider.empty": "Geen providers gevonden",
|
||||
"dialog.provider.group.popular": "Populair",
|
||||
"dialog.provider.group.other": "Overig",
|
||||
"dialog.provider.tag.recommended": "Aanbevolen",
|
||||
"dialog.provider.opencode.note": "Geselecteerde modellen inclusief Claude, GPT, Gemini en meer",
|
||||
"dialog.provider.opencode.tagline": "Betrouwbare geoptimaliseerde modellen",
|
||||
"dialog.provider.opencodeGo.tagline": "Goedkoop abonnement voor iedereen",
|
||||
"dialog.provider.anthropic.note": "Directe toegang tot Claude modellen, inclusief Pro en Max",
|
||||
"dialog.provider.copilot.note": "AI-modellen voor codeerassistentie via GitHub Copilot",
|
||||
"dialog.provider.openai.note": "GPT modellen voor snelle, capabele algemene AI-taken",
|
||||
"dialog.provider.google.note": "Gemini modellen voor snelle, gestructureerde antwoorden",
|
||||
"dialog.provider.openrouter.note": "Toegang tot alle ondersteunde modellen via één provider",
|
||||
"dialog.provider.vercel.note": "Geünificeerde toegang tot AI-modellen met slimme routering",
|
||||
|
||||
"dialog.model.select.title": "Selecteer model",
|
||||
"dialog.model.search.placeholder": "Zoek modellen",
|
||||
"dialog.model.empty": "Geen modelresultaten",
|
||||
"dialog.model.manage": "Modellen beheren",
|
||||
"dialog.model.manage.description": "Pas aan welke modellen in de modelkiezer verschijnen.",
|
||||
"dialog.model.manage.provider.toggle": "Alle {{provider}} modellen in-/uitschakelen",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Gratis modellen aangeboden door Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Meer modellen toevoegen van populaire providers",
|
||||
|
||||
"dialog.provider.viewAll": "Toon meer providers",
|
||||
|
||||
"provider.connect.title": "{{provider}} verbinden",
|
||||
"provider.connect.title.anthropicProMax": "Inloggen met Claude Pro/Max",
|
||||
"provider.connect.selectMethod": "Selecteer inlogmethode voor {{provider}}.",
|
||||
"provider.connect.method.apiKey": "API key",
|
||||
"provider.connect.status.inProgress": "Autorisatie in uitvoering...",
|
||||
"provider.connect.status.waiting": "Wachten op autorisatie...",
|
||||
"provider.connect.status.failed": "Autorisatie mislukt: {{error}}",
|
||||
"provider.connect.apiKey.description":
|
||||
"Voer uw {{provider}} API key in om uw account te verbinden en {{provider}} modellen te gebruiken in Kilo.",
|
||||
"provider.connect.apiKey.label": "{{provider}} API key",
|
||||
"provider.connect.apiKey.placeholder": "API key",
|
||||
"provider.connect.apiKey.required": "API key is vereist",
|
||||
"provider.connect.opencodeZen.line1":
|
||||
"OpenCode Zen geeft u toegang tot een geselecteerde set betrouwbare en geoptimaliseerde modellen voor coderingsagenten.",
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"Met een enkele API key krijgt u toegang tot modellen zoals Claude, GPT, Gemini, GLM en meer.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Bezoek ",
|
||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||
"provider.connect.opencodeZen.visit.suffix": " om uw API key te verkrijgen.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Bezoek ",
|
||||
"provider.connect.oauth.code.visit.link": "deze link",
|
||||
"provider.connect.oauth.code.visit.suffix":
|
||||
" om uw autorisatiecode te verkrijgen, uw account te verbinden en {{provider}} modellen te gebruiken in Kilo.",
|
||||
"provider.connect.oauth.code.label": "{{method}} autorisatiecode",
|
||||
"provider.connect.oauth.code.placeholder": "Autorisatiecode",
|
||||
"provider.connect.oauth.code.required": "Autorisatiecode is vereist",
|
||||
"provider.connect.oauth.code.invalid": "Ongeldige autorisatiecode",
|
||||
"provider.connect.oauth.auto.visit.prefix": "Bezoek ",
|
||||
"provider.connect.oauth.auto.visit.link": "deze link",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" en voer de onderstaande code in om uw account te verbinden en {{provider}} modellen te gebruiken in Kilo.",
|
||||
"provider.connect.oauth.auto.confirmationCode": "Bevestigingscode",
|
||||
"provider.connect.toast.connected.title": "{{provider}} verbonden",
|
||||
"provider.connect.toast.connected.description": "{{provider}} modellen zijn nu beschikbaar om te gebruiken.",
|
||||
|
||||
"provider.custom.title": "Aangepaste provider",
|
||||
"provider.custom.description.prefix": "Configureer een OpenAI-compatibele provider. Zie de ",
|
||||
"provider.custom.description.link": "provider configuratiedocumentatie",
|
||||
"provider.custom.description.suffix": ".",
|
||||
"provider.custom.field.providerID.label": "Provider ID",
|
||||
"provider.custom.field.providerID.placeholder": "myprovider",
|
||||
"provider.custom.field.providerID.description": "Kleine letters, cijfers, koppeltekens of underscores",
|
||||
"provider.custom.field.name.label": "Weergavenaam",
|
||||
"provider.custom.field.name.placeholder": "Mijn AI Provider",
|
||||
"provider.custom.field.baseURL.label": "Basis-URL",
|
||||
"provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
|
||||
"provider.custom.field.apiKey.label": "API key",
|
||||
"provider.custom.field.apiKey.placeholder": "API key",
|
||||
"provider.custom.field.apiKey.description": "Optioneel. Laat leeg als u auth via headers beheert.",
|
||||
"provider.custom.models.label": "Modellen",
|
||||
"provider.custom.models.id.label": "ID",
|
||||
"provider.custom.models.id.placeholder": "model-id",
|
||||
"provider.custom.models.name.label": "Naam",
|
||||
"provider.custom.models.name.placeholder": "Weergavenaam",
|
||||
"provider.custom.models.remove": "Model verwijderen",
|
||||
"provider.custom.models.add": "Model toevoegen",
|
||||
"provider.custom.headers.label": "Headers (optioneel)",
|
||||
"provider.custom.headers.key.label": "Header",
|
||||
"provider.custom.headers.key.placeholder": "Header-Naam",
|
||||
"provider.custom.headers.value.label": "Waarde",
|
||||
"provider.custom.headers.value.placeholder": "waarde",
|
||||
"provider.custom.headers.remove": "Header verwijderen",
|
||||
"provider.custom.headers.add": "Header toevoegen",
|
||||
"provider.custom.error.providerID.required": "Provider ID is vereist",
|
||||
"provider.custom.error.providerID.format": "Gebruik kleine letters, cijfers, koppeltekens of underscores",
|
||||
"provider.custom.error.providerID.exists": "Deze provider ID bestaat al",
|
||||
"provider.custom.error.name.required": "Weergavenaam is vereist",
|
||||
"provider.custom.error.baseURL.required": "Basis-URL is vereist",
|
||||
"provider.custom.error.baseURL.format": "Moet beginnen met http:// of https://",
|
||||
"provider.custom.error.required": "Vereist",
|
||||
"provider.custom.error.duplicate": "Duplicaat",
|
||||
|
||||
"provider.disconnect.toast.disconnected.title": "{{provider}} losgekoppeld",
|
||||
"provider.disconnect.toast.disconnected.description": "{{provider}} modellen zijn niet langer beschikbaar.",
|
||||
|
||||
"model.tag.free": "Gratis",
|
||||
"model.tag.latest": "Nieuwste",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
"model.provider.openai": "OpenAI",
|
||||
"model.provider.google": "Google",
|
||||
"model.provider.xai": "xAI",
|
||||
"model.provider.meta": "Meta",
|
||||
"model.input.text": "tekst",
|
||||
"model.input.image": "afbeelding",
|
||||
"model.input.audio": "audio",
|
||||
"model.input.video": "video",
|
||||
"model.input.pdf": "pdf",
|
||||
"model.tooltip.allows": "Staat toe: {{inputs}}",
|
||||
"model.tooltip.reasoning.allowed": "Staat redenering toe",
|
||||
"model.tooltip.reasoning.none": "Geen redenering",
|
||||
"model.tooltip.context": "Contextlimiet {{limit}}",
|
||||
|
||||
"common.search.placeholder": "Zoeken",
|
||||
"common.goBack": "Ga terug",
|
||||
"common.goForward": "Ga vooruit",
|
||||
"common.loading": "Laden",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Annuleren",
|
||||
"common.open": "Openen",
|
||||
"common.connect": "Verbinden",
|
||||
"common.disconnect": "Loskoppelen",
|
||||
"common.submit": "Verzenden",
|
||||
"common.save": "Opslaan",
|
||||
"common.saving": "Opslaan...",
|
||||
"common.default": "Standaard",
|
||||
"common.attachment": "bijlage",
|
||||
|
||||
"prompt.placeholder.shell": "Voer shell-opdracht in...",
|
||||
"prompt.placeholder.normal": 'Vraag alles... "{{example}}"',
|
||||
"prompt.placeholder.simple": "Vraag alles...",
|
||||
"prompt.placeholder.summarizeComments": "Reacties samenvatten...",
|
||||
"prompt.placeholder.summarizeComment": "Reactie samenvatten...",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "esc om te verlaten",
|
||||
|
||||
"prompt.example.1": "Fix een TODO in de codebase",
|
||||
"prompt.example.2": "Wat is de tech stack van dit project?",
|
||||
"prompt.example.3": "Repareer kapotte tests",
|
||||
"prompt.example.4": "Leg uit hoe authenticatie werkt",
|
||||
"prompt.example.5": "Vind en herstel beveiligingskwetsbaarheden",
|
||||
"prompt.example.6": "Voeg unittests toe voor de gebruikersservice",
|
||||
"prompt.example.7": "Herschrijf deze functie om deze beter leesbaar te maken",
|
||||
"prompt.example.8": "Wat betekent deze fout?",
|
||||
"prompt.example.9": "Help me met het debuggen van dit probleem",
|
||||
"prompt.example.10": "Genereer API-documentatie",
|
||||
"prompt.example.11": "Optimaliseer databasequery's",
|
||||
"prompt.example.12": "Voeg invoervalidatie toe",
|
||||
"prompt.example.13": "Maak een nieuwe component voor...",
|
||||
"prompt.example.14": "Hoe implementeer ik dit project?",
|
||||
"prompt.example.15": "Bekijk mijn code voor best practices",
|
||||
"prompt.example.16": "Voeg foutafhandeling toe aan deze functie",
|
||||
"prompt.example.17": "Leg dit regex-patroon uit",
|
||||
"prompt.example.18": "Zet dit om naar TypeScript",
|
||||
"prompt.example.19": "Voeg logging toe in de hele codebase",
|
||||
"prompt.example.20": "Welke afhankelijkheden zijn verouderd?",
|
||||
"prompt.example.21": "Help me een migratiescript te schrijven",
|
||||
"prompt.example.22": "Implementeer caching voor dit eindpunt",
|
||||
"prompt.example.23": "Voeg paginering toe aan deze lijst",
|
||||
"prompt.example.24": "Maak een CLI-opdracht voor...",
|
||||
"prompt.example.25": "Hoe werken omgevingsvariabelen hier?",
|
||||
|
||||
"prompt.popover.emptyResults": "Geen overeenkomende resultaten",
|
||||
"prompt.popover.emptyCommands": "Geen overeenkomende opdrachten",
|
||||
"prompt.dropzone.label": "Sleep afbeeldingen of PDF's hierheen",
|
||||
"prompt.dropzone.file.label": "Sleep om bestand te @vermelden",
|
||||
"prompt.slash.badge.custom": "aangepast",
|
||||
"prompt.slash.badge.skill": "skill",
|
||||
"prompt.slash.badge.mcp": "mcp",
|
||||
"prompt.context.active": "actief",
|
||||
"prompt.context.includeActiveFile": "Neem actief bestand op",
|
||||
"prompt.context.removeActiveFile": "Verwijder actief bestand uit context",
|
||||
"prompt.context.removeFile": "Verwijder bestand uit context",
|
||||
"prompt.action.attachFile": "Bestand toevoegen",
|
||||
"prompt.attachment.remove": "Bijlage verwijderen",
|
||||
"prompt.action.send": "Verzenden",
|
||||
"prompt.action.stop": "Stop",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Niet-ondersteunde plakbewerking",
|
||||
"prompt.toast.pasteUnsupported.description": "Hier kunnen alleen afbeeldingen of PDF's worden geplakt.",
|
||||
"prompt.toast.modelAgentRequired.title": "Selecteer een agent en model",
|
||||
"prompt.toast.modelAgentRequired.description": "Kies een agent en model voordat u een prompt verzendt.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Maken van worktree mislukt",
|
||||
"prompt.toast.sessionCreateFailed.title": "Maken van sessie mislukt",
|
||||
"prompt.toast.shellSendFailed.title": "Verzenden van shell-opdracht mislukt",
|
||||
"prompt.toast.commandSendFailed.title": "Verzenden van opdracht mislukt",
|
||||
"prompt.toast.promptSendFailed.title": "Verzenden van prompt mislukt",
|
||||
"prompt.toast.promptSendFailed.description": "Kan sessie niet ophalen",
|
||||
|
||||
"dialog.mcp.title": "MCPs",
|
||||
"dialog.mcp.description": "{{enabled}} van {{total}} ingeschakeld",
|
||||
"dialog.mcp.empty": "Geen MCP's geconfigureerd",
|
||||
|
||||
"dialog.lsp.empty": "LSP's automatisch gedetecteerd aan de hand van bestandstypen",
|
||||
"dialog.plugins.empty": "Plug-ins geconfigureerd in opencode.json",
|
||||
|
||||
"mcp.status.connected": "verbonden",
|
||||
"mcp.status.failed": "mislukt",
|
||||
"mcp.status.needs_auth": "authenticatie vereist",
|
||||
"mcp.status.disabled": "uitgeschakeld",
|
||||
|
||||
"dialog.fork.empty": "Geen berichten om van af te splitsen",
|
||||
|
||||
"dialog.directory.search.placeholder": "Zoek mappen",
|
||||
"dialog.directory.empty": "Geen mappen gevonden",
|
||||
|
||||
"dialog.server.title": "Servers",
|
||||
"dialog.server.description": "Wissel met welke Kilo server deze app verbindt.",
|
||||
"dialog.server.search.placeholder": "Zoek servers",
|
||||
"dialog.server.empty": "Nog geen servers",
|
||||
"dialog.server.add.title": "Server toevoegen",
|
||||
"dialog.server.add.url": "Serveradres",
|
||||
"dialog.server.add.placeholder": "http://localhost:4096",
|
||||
"dialog.server.add.error": "Kan geen verbinding maken met server",
|
||||
"dialog.server.add.checking": "Controleren...",
|
||||
"dialog.server.add.button": "Server toevoegen",
|
||||
"dialog.server.add.name": "Servernaam (optioneel)",
|
||||
"dialog.server.add.namePlaceholder": "Localhost",
|
||||
"dialog.server.add.username": "Gebruikersnaam (optioneel)",
|
||||
"dialog.server.add.password": "Wachtwoord (optioneel)",
|
||||
"dialog.server.edit.title": "Server bewerken",
|
||||
"dialog.server.default.title": "Standaardserver",
|
||||
"dialog.server.default.description":
|
||||
"Maak verbinding met deze server bij de lancering van de app in plaats van een lokale server te starten. Vereist een herstart.",
|
||||
"dialog.server.default.none": "Geen server geselecteerd",
|
||||
"dialog.server.default.set": "Huidige server als standaard instellen",
|
||||
"dialog.server.default.clear": "Wissen",
|
||||
"dialog.server.action.remove": "Server verwijderen",
|
||||
|
||||
"dialog.server.menu.edit": "Bewerken",
|
||||
"dialog.server.menu.default": "Als standaard instellen",
|
||||
"dialog.server.menu.defaultRemove": "Standaard verwijderen",
|
||||
"dialog.server.menu.delete": "Verwijderen",
|
||||
"dialog.server.current": "Huidige Server",
|
||||
"dialog.server.status.default": "Standaard",
|
||||
|
||||
"dialog.project.edit.title": "Project bewerken",
|
||||
"dialog.project.edit.name": "Naam",
|
||||
"dialog.project.edit.icon": "Icoon",
|
||||
"dialog.project.edit.icon.alt": "Projecticoon",
|
||||
"dialog.project.edit.icon.hint": "Klik of sleep een afbeelding",
|
||||
"dialog.project.edit.icon.recommended": "Aanbevolen: 128x128px",
|
||||
"dialog.project.edit.color": "Kleur",
|
||||
"dialog.project.edit.color.select": "Selecteer {{color}} kleur",
|
||||
"dialog.project.edit.worktree.startup": "Werkruimte opstartscript",
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Wordt uitgevoerd na het maken van een nieuwe werkruimte (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "bijv. bun install",
|
||||
|
||||
"dialog.releaseNotes.action.getStarted": "Aan de slag",
|
||||
"dialog.releaseNotes.action.next": "Volgende",
|
||||
"dialog.releaseNotes.action.hideFuture": "Toon deze niet in de toekomst",
|
||||
"dialog.releaseNotes.media.alt": "Releasevoorbeeld",
|
||||
|
||||
"context.breakdown.title": "Contextverdeling",
|
||||
"context.breakdown.note": 'Geschatte verdeling van invoertokens. "Overig" bevat tooldefinities en overhead.',
|
||||
"context.breakdown.system": "Systeem",
|
||||
"context.breakdown.user": "Gebruiker",
|
||||
"context.breakdown.assistant": "Assistent",
|
||||
"context.breakdown.tool": "Tool-aanroepen",
|
||||
"context.breakdown.other": "Overig",
|
||||
|
||||
"context.systemPrompt.title": "Systeemprompt",
|
||||
"context.rawMessages.title": "Ruwe berichten",
|
||||
|
||||
"context.stats.session": "Sessie",
|
||||
"context.stats.messages": "Berichten",
|
||||
"context.stats.provider": "Provider",
|
||||
"context.stats.model": "Model",
|
||||
"context.stats.limit": "Contextlimiet",
|
||||
"context.stats.totalTokens": "Totaal Tokens",
|
||||
"context.stats.usage": "Gebruik",
|
||||
"context.stats.inputTokens": "Invoertokens",
|
||||
"context.stats.outputTokens": "Uitvoertokens",
|
||||
"context.stats.reasoningTokens": "Redeneertokens",
|
||||
"context.stats.cacheTokens": "Cachetokens (lezen/schrijven)",
|
||||
"context.stats.userMessages": "Gebruikersberichten",
|
||||
"context.stats.assistantMessages": "Assistentberichten",
|
||||
"context.stats.totalCost": "Totale Kosten",
|
||||
"context.stats.sessionCreated": "Sessie Aangemaakt",
|
||||
"context.stats.lastActivity": "Laatste Activiteit",
|
||||
|
||||
"context.usage.tokens": "Tokens",
|
||||
"context.usage.usage": "Gebruik",
|
||||
"context.usage.cost": "Kosten",
|
||||
"context.usage.clickToView": "Klik om context te bekijken",
|
||||
"context.usage.view": "Contextgebruik bekijken",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.bs": "Bosanski",
|
||||
"language.nl": "Nederlands",
|
||||
"language.th": "ไทย",
|
||||
"language.tr": "Türkçe",
|
||||
|
||||
"toast.language.title": "Taal",
|
||||
"toast.language.description": "Omgeschakeld naar {{language}}",
|
||||
|
||||
"toast.theme.title": "Thema gewisseld",
|
||||
"toast.scheme.title": "Kleurenschema",
|
||||
|
||||
"toast.workspace.enabled.title": "Werkruimtes ingeschakeld",
|
||||
"toast.workspace.enabled.description": "Meerdere worktrees worden nu weergegeven in de zijbalk",
|
||||
"toast.workspace.disabled.title": "Werkruimtes uitgeschakeld",
|
||||
"toast.workspace.disabled.description": "Alleen de hoofdworktree wordt weergegeven in de zijbalk",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Machtigingen automatisch accepteren",
|
||||
"toast.permissions.autoaccept.on.description": "Toestemmingsverzoeken worden automatisch goedgekeurd",
|
||||
"toast.permissions.autoaccept.off.title": "Gestopt met automatisch accepteren van machtigingen",
|
||||
"toast.permissions.autoaccept.off.description": "Toestemmingsverzoeken vereisen goedkeuring",
|
||||
|
||||
"toast.model.none.title": "Geen model geselecteerd",
|
||||
"toast.model.none.description": "Verbind een provider om deze sessie samen te vatten",
|
||||
|
||||
"toast.file.loadFailed.title": "Laden van bestand mislukt",
|
||||
"toast.file.listFailed.title": "Lijst met bestanden ophalen mislukt",
|
||||
|
||||
"toast.context.noLineSelection.title": "Geen regelselectie",
|
||||
"toast.context.noLineSelection.description": "Selecteer eerst een regelbereik in een bestandstabblad.",
|
||||
|
||||
"toast.session.share.copyFailed.title": "Kopiëren van URL naar klembord mislukt",
|
||||
"toast.session.share.success.title": "Sessie gedeeld",
|
||||
"toast.session.share.success.description": "Deel-URL naar klembord gekopieerd!",
|
||||
"toast.session.share.failed.title": "Delen van sessie mislukt",
|
||||
"toast.session.share.failed.description": "Er is een fout opgetreden bij het delen van de sessie",
|
||||
|
||||
"toast.session.unshare.success.title": "Sessie niet meer gedeeld",
|
||||
"toast.session.unshare.success.description": "Sessie succesvol niet meer gedeeld!",
|
||||
"toast.session.unshare.failed.title": "Sessie niet meer delen mislukt",
|
||||
"toast.session.unshare.failed.description":
|
||||
"Er is een fout opgetreden bij het ongedaan maken van delen van de sessie",
|
||||
|
||||
"toast.session.listFailed.title": "Laden van sessies voor {{project}} mislukt",
|
||||
"toast.project.reloadFailed.title": "Herladen van {{project}} mislukt",
|
||||
|
||||
"toast.update.title": "Update beschikbaar",
|
||||
"toast.update.description": "Een nieuwe versie van Kilo ({{version}}) is nu beschikbaar om te installeren.",
|
||||
"toast.update.action.installRestart": "Installeren en herstarten",
|
||||
"toast.update.action.notYet": "Nog niet",
|
||||
|
||||
"error.page.title": "Er is iets misgegaan",
|
||||
"error.page.description": "Er is een fout opgetreden tijdens het laden van de applicatie.",
|
||||
"error.page.details.label": "Foutdetails",
|
||||
"error.page.action.restart": "Herstarten",
|
||||
"error.page.action.checking": "Controleren...",
|
||||
"error.page.action.checkUpdates": "Controleren op updates",
|
||||
"error.page.action.updateTo": "Updaten naar {{version}}",
|
||||
"error.page.report.prefix": "Meld deze fout alstublieft aan het Kilo team",
|
||||
"error.page.report.discord": "op Discord",
|
||||
"error.page.version": "Versie: {{version}}",
|
||||
|
||||
"error.dev.rootNotFound":
|
||||
"Root-element niet gevonden. Bent u vergeten dit toe te voegen aan uw index.html? Of misschien is het id-attribuut verkeerd gespeld?",
|
||||
|
||||
"error.globalSync.connectFailed": "Kon geen verbinding maken met server. Draait er een server op `{{url}}`?",
|
||||
"directory.error.invalidUrl": "Ongeldige map in URL.",
|
||||
|
||||
"error.chain.unknown": "Onbekende fout",
|
||||
"error.server.invalidConfiguration": "Ongeldige configuratie",
|
||||
"error.chain.causedBy": "Veroorzaakt door:",
|
||||
"error.chain.apiError": "API-fout",
|
||||
"error.chain.status": "Status: {{status}}",
|
||||
"error.chain.retryable": "Opnieuw te proberen: {{retryable}}",
|
||||
"error.chain.responseBody": "Reactie-body:\n{{body}}",
|
||||
"error.chain.didYouMean": "Bedoelde u: {{suggestions}}",
|
||||
"error.chain.modelNotFound": "Model niet gevonden: {{provider}}/{{model}}",
|
||||
"error.chain.checkConfig": "Controleer uw config (opencode.json) provider/modelnamen",
|
||||
"error.chain.mcpFailed": 'MCP-server "{{name}}" is mislukt. Let op, Kilo ondersteunt nog geen MCP-authenticatie.',
|
||||
"error.chain.providerAuthFailed": "Providerauthenticatie mislukt ({{provider}}): {{message}}",
|
||||
"error.chain.providerInitFailed":
|
||||
'Initialiseren van provider "{{provider}}" mislukt. Controleer referenties en configuratie.',
|
||||
"error.chain.configJsonInvalid": "Configuratiebestand op {{path}} is geen geldige JSON(C)",
|
||||
"error.chain.configJsonInvalidWithMessage": "Configuratiebestand op {{path}} is geen geldige JSON(C): {{message}}",
|
||||
"error.chain.configDirectoryTypo":
|
||||
'Map "{{dir}}" in {{path}} is niet geldig. Hernoem de map naar "{{suggestion}}" of verwijder deze. Dit is een veelvoorkomende typfout.',
|
||||
"error.chain.configFrontmatterError": "Parsen van frontmatter mislukt in {{path}}:\n{{message}}",
|
||||
"error.chain.configInvalid": "Configuratiebestand op {{path}} is ongeldig",
|
||||
"error.chain.configInvalidWithMessage": "Configuratiebestand op {{path}} is ongeldig: {{message}}",
|
||||
|
||||
"notification.permission.title": "Machtiging vereist",
|
||||
"notification.permission.description": "{{sessionTitle}} in {{projectName}} heeft machtiging nodig",
|
||||
"notification.question.title": "Vraag",
|
||||
"notification.question.description": "{{sessionTitle}} in {{projectName}} heeft een vraag",
|
||||
"notification.action.goToSession": "Ga naar sessie",
|
||||
|
||||
"notification.session.responseReady.title": "Antwoord gereed",
|
||||
"notification.session.error.title": "Sessiefout",
|
||||
"notification.session.error.fallbackDescription": "Er is een fout opgetreden",
|
||||
|
||||
"home.recentProjects": "Recente projecten",
|
||||
"home.empty.title": "Geen recente projecten",
|
||||
"home.empty.description": "Ga aan de slag door een lokaal project te openen",
|
||||
|
||||
"session.tab.session": "Sessie",
|
||||
"session.tab.review": "Review",
|
||||
"session.tab.context": "Context",
|
||||
"session.panel.reviewAndFiles": "Review en bestanden",
|
||||
"session.review.filesChanged": "{{count}} Bestanden Gewijzigd",
|
||||
"session.review.change.one": "Wijziging",
|
||||
"session.review.change.other": "Wijzigingen",
|
||||
"session.review.loadingChanges": "Wijzigingen laden...",
|
||||
"session.review.empty": "Nog geen wijzigingen in deze sessie",
|
||||
"session.review.noVcs": "Geen Git Version Control System gedetecteerd, wijzigingen niet weergegeven",
|
||||
"session.review.noSnapshot":
|
||||
"Snapshot-tracking is uitgeschakeld in configuratie, dus sessiewijzigingen zijn niet beschikbaar",
|
||||
"session.review.noChanges": "Geen wijzigingen",
|
||||
|
||||
"session.files.selectToOpen": "Selecteer een bestand om te openen",
|
||||
"session.files.all": "Alle bestanden",
|
||||
"session.files.empty": "Geen bestanden",
|
||||
"session.files.binaryContent": "Binair bestand (inhoud kan niet worden weergegeven)",
|
||||
|
||||
"session.messages.renderEarlier": "Eerdere berichten renderen",
|
||||
"session.messages.loadingEarlier": "Eerdere berichten laden...",
|
||||
"session.messages.loadEarlier": "Eerdere berichten laden",
|
||||
"session.messages.loading": "Berichten laden...",
|
||||
"session.messages.jumpToLatest": "Spring naar nieuwste",
|
||||
|
||||
"session.context.addToContext": "Voeg {{selection}} toe aan context",
|
||||
"session.todo.title": "To-do's",
|
||||
"session.todo.collapse": "Inklappen",
|
||||
"session.todo.expand": "Uitklappen",
|
||||
|
||||
"session.new.title": "Bouw wat u wilt",
|
||||
"session.new.worktree.main": "Hoofdbranch",
|
||||
"session.new.worktree.mainWithBranch": "Hoofdbranch ({{branch}})",
|
||||
"session.new.worktree.create": "Nieuwe worktree maken",
|
||||
"session.new.lastModified": "Laatst gewijzigd",
|
||||
|
||||
"session.header.search.placeholder": "Zoek {{project}}",
|
||||
"session.header.searchFiles": "Zoek bestanden",
|
||||
"session.header.openIn": "Openen in",
|
||||
"session.header.open.action": "Open {{app}}",
|
||||
"session.header.open.ariaLabel": "Openen in {{app}}",
|
||||
"session.header.open.menu": "Open-opties",
|
||||
"session.header.open.copyPath": "Pad kopiëren",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Serverconfiguraties",
|
||||
"status.popover.tab.servers": "Servers",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plug-ins",
|
||||
"status.popover.action.manageServers": "Servers beheren",
|
||||
|
||||
"session.share.popover.title": "Op web publiceren",
|
||||
"session.share.popover.description.shared":
|
||||
"Deze sessie is openbaar op het web. Het is toegankelijk voor iedereen met de link.",
|
||||
"session.share.popover.description.unshared":
|
||||
"Sessie openbaar delen op het web. Het zal toegankelijk zijn voor iedereen met de link.",
|
||||
"session.share.action.share": "Delen",
|
||||
"session.share.action.publish": "Publiceren",
|
||||
"session.share.action.publishing": "Publiceren...",
|
||||
"session.share.action.unpublish": "Publicatie ongedaan maken",
|
||||
"session.share.action.unpublishing": "Publicatie ongedaan maken...",
|
||||
"session.share.action.view": "Bekijken",
|
||||
"session.share.copy.copied": "Gekopieerd",
|
||||
"session.share.copy.copyLink": "Link kopiëren",
|
||||
|
||||
"lsp.tooltip.none": "Geen LSP-servers",
|
||||
"lsp.label.connected": "{{count}} LSP",
|
||||
|
||||
"prompt.loading": "Prompt laden...",
|
||||
"terminal.loading": "Terminal laden...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Terminal sluiten",
|
||||
"terminal.connectionLost.title": "Verbinding verbroken",
|
||||
"terminal.connectionLost.description":
|
||||
"De terminalverbinding is onderbroken. Dit kan gebeuren wanneer de server herstart.",
|
||||
|
||||
"common.closeTab": "Tabblad sluiten",
|
||||
"common.dismiss": "Negeren",
|
||||
"common.moreCountSuffix": " (nog +{{count}})",
|
||||
"common.requestFailed": "Verzoek mislukt",
|
||||
"common.moreOptions": "Meer opties",
|
||||
"common.learnMore": "Meer informatie",
|
||||
"common.rename": "Hernoemen",
|
||||
"common.reset": "Reset",
|
||||
"common.archive": "Archiveren",
|
||||
"common.delete": "Verwijderen",
|
||||
"common.close": "Sluiten",
|
||||
"common.edit": "Bewerken",
|
||||
"common.loadMore": "Meer laden",
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"common.time.justNow": "Zojuist",
|
||||
"common.time.minutesAgo.short": "{{count}}m geleden",
|
||||
"common.time.hoursAgo.short": "{{count}}u geleden",
|
||||
"common.time.daysAgo.short": "{{count}}d geleden",
|
||||
|
||||
"sidebar.menu.toggle": "Menu in-/uitschakelen",
|
||||
"sidebar.nav.projectsAndSessions": "Projecten en sessies",
|
||||
"sidebar.settings": "Instellingen",
|
||||
"sidebar.help": "Help",
|
||||
"sidebar.workspaces.enable": "Werkruimtes inschakelen",
|
||||
"sidebar.workspaces.disable": "Werkruimtes uitschakelen",
|
||||
"sidebar.gettingStarted.title": "Aan de slag",
|
||||
"sidebar.gettingStarted.line1": "Kilo bevat gratis modellen zodat u direct kunt beginnen.",
|
||||
"sidebar.gettingStarted.line2": "Verbind een provider om modellen te gebruiken, incl. Claude, GPT, Gemini etc.",
|
||||
"sidebar.project.recentSessions": "Recente sessies",
|
||||
"sidebar.project.viewAllSessions": "Bekijk alle sessies",
|
||||
"sidebar.project.clearNotifications": "Meldingen wissen",
|
||||
|
||||
"app.name.desktop": "Kilo Desktop",
|
||||
|
||||
"settings.section.desktop": "Desktop",
|
||||
"settings.section.server": "Server",
|
||||
"settings.tab.general": "Algemeen",
|
||||
"settings.tab.shortcuts": "Sneltoetsen",
|
||||
"settings.desktop.section.wsl": "WSL",
|
||||
"settings.desktop.wsl.title": "WSL-integratie",
|
||||
"settings.desktop.wsl.description": "Draai de Kilo server binnen WSL op Windows.",
|
||||
|
||||
"settings.general.section.appearance": "Uiterlijk",
|
||||
"settings.general.section.notifications": "Systeemmeldingen",
|
||||
"settings.general.section.updates": "Updates",
|
||||
"settings.general.section.sounds": "Geluidseffecten",
|
||||
"settings.general.section.feed": "Feed",
|
||||
"settings.general.section.display": "Weergave",
|
||||
|
||||
"settings.general.row.language.title": "Taal",
|
||||
"settings.general.row.language.description": "Wijzig de weergavetaal voor Kilo",
|
||||
"settings.general.row.appearance.title": "Uiterlijk",
|
||||
"settings.general.row.appearance.description": "Pas aan hoe Kilo eruitziet op uw apparaat",
|
||||
"settings.general.row.theme.title": "Thema",
|
||||
"settings.general.row.theme.description": "Pas het thema van Kilo aan.",
|
||||
"settings.general.row.font.title": "Lettertype",
|
||||
"settings.general.row.font.description": "Pas het monospace-lettertype in codeblokken aan",
|
||||
"settings.general.row.reasoningSummaries.title": "Redeneersamenvattingen weergeven",
|
||||
"settings.general.row.reasoningSummaries.description": "Toon model-redeneersamenvattingen in de tijdlijn",
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Shell-toolonderdelen uitklappen",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
"Toon shell-toolonderdelen standaard uitgeklapt in de tijdlijn",
|
||||
"settings.general.row.editToolPartsExpanded.title": "Bewerk-toolonderdelen uitklappen",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Toon bewerk-, schrijf- en patch-toolonderdelen standaard uitgeklapt in de tijdlijn",
|
||||
|
||||
"settings.general.row.wayland.title": "Gebruik native Wayland",
|
||||
"settings.general.row.wayland.description": "Schakel X11-terugval uit op Wayland. Vereist een herstart.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
"Op Linux met monitoren met gemengde verversingssnelheden kan native Wayland stabieler zijn.",
|
||||
|
||||
"settings.general.row.releaseNotes.title": "Release notes",
|
||||
"settings.general.row.releaseNotes.description": "Toon Wat is nieuw pop-ups na updates",
|
||||
|
||||
"settings.updates.row.startup.title": "Controleren op updates bij opstarten",
|
||||
"settings.updates.row.startup.description": "Automatisch op updates controleren wanneer Kilo start",
|
||||
"settings.updates.row.check.title": "Controleren op updates",
|
||||
"settings.updates.row.check.description": "Handmatig op updates controleren en installeren indien beschikbaar",
|
||||
"settings.updates.action.checkNow": "Nu controleren",
|
||||
"settings.updates.action.checking": "Controleren...",
|
||||
"settings.updates.toast.latest.title": "U bent up-to-date",
|
||||
"settings.updates.toast.latest.description": "U draait de nieuwste versie van Kilo.",
|
||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||
"font.option.cascadiaCode": "Cascadia Code",
|
||||
"font.option.firaCode": "Fira Code",
|
||||
"font.option.hack": "Hack",
|
||||
"font.option.inconsolata": "Inconsolata",
|
||||
"font.option.intelOneMono": "Intel One Mono",
|
||||
"font.option.iosevka": "Iosevka",
|
||||
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||
"font.option.mesloLgs": "Meslo LGS",
|
||||
"font.option.robotoMono": "Roboto Mono",
|
||||
"font.option.sourceCodePro": "Source Code Pro",
|
||||
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||
"font.option.geistMono": "Geist Mono",
|
||||
"sound.option.none": "Geen",
|
||||
"sound.option.alert01": "Melding 01",
|
||||
"sound.option.alert02": "Melding 02",
|
||||
"sound.option.alert03": "Melding 03",
|
||||
"sound.option.alert04": "Melding 04",
|
||||
"sound.option.alert05": "Melding 05",
|
||||
"sound.option.alert06": "Melding 06",
|
||||
"sound.option.alert07": "Melding 07",
|
||||
"sound.option.alert08": "Melding 08",
|
||||
"sound.option.alert09": "Melding 09",
|
||||
"sound.option.alert10": "Melding 10",
|
||||
"sound.option.bipbop01": "Bip-bop 01",
|
||||
"sound.option.bipbop02": "Bip-bop 02",
|
||||
"sound.option.bipbop03": "Bip-bop 03",
|
||||
"sound.option.bipbop04": "Bip-bop 04",
|
||||
"sound.option.bipbop05": "Bip-bop 05",
|
||||
"sound.option.bipbop06": "Bip-bop 06",
|
||||
"sound.option.bipbop07": "Bip-bop 07",
|
||||
"sound.option.bipbop08": "Bip-bop 08",
|
||||
"sound.option.bipbop09": "Bip-bop 09",
|
||||
"sound.option.bipbop10": "Bip-bop 10",
|
||||
"sound.option.staplebops01": "Staplebops 01",
|
||||
"sound.option.staplebops02": "Staplebops 02",
|
||||
"sound.option.staplebops03": "Staplebops 03",
|
||||
"sound.option.staplebops04": "Staplebops 04",
|
||||
"sound.option.staplebops05": "Staplebops 05",
|
||||
"sound.option.staplebops06": "Staplebops 06",
|
||||
"sound.option.staplebops07": "Staplebops 07",
|
||||
"sound.option.nope01": "Nee 01",
|
||||
"sound.option.nope02": "Nee 02",
|
||||
"sound.option.nope03": "Nee 03",
|
||||
"sound.option.nope04": "Nee 04",
|
||||
"sound.option.nope05": "Nee 05",
|
||||
"sound.option.nope06": "Nee 06",
|
||||
"sound.option.nope07": "Nee 07",
|
||||
"sound.option.nope08": "Nee 08",
|
||||
"sound.option.nope09": "Nee 09",
|
||||
"sound.option.nope10": "Nee 10",
|
||||
"sound.option.nope11": "Nee 11",
|
||||
"sound.option.nope12": "Nee 12",
|
||||
"sound.option.yup01": "Ja 01",
|
||||
"sound.option.yup02": "Ja 02",
|
||||
"sound.option.yup03": "Ja 03",
|
||||
"sound.option.yup04": "Ja 04",
|
||||
"sound.option.yup05": "Ja 05",
|
||||
"sound.option.yup06": "Ja 06",
|
||||
|
||||
"settings.general.notifications.agent.title": "Agent",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Toon systeemmelding wanneer de agent klaar is of aandacht nodig heeft",
|
||||
"settings.general.notifications.permissions.title": "Machtigingen",
|
||||
"settings.general.notifications.permissions.description": "Toon systeemmelding wanneer een machtiging vereist is",
|
||||
"settings.general.notifications.errors.title": "Fouten",
|
||||
"settings.general.notifications.errors.description": "Toon systeemmelding wanneer er een fout optreedt",
|
||||
|
||||
"settings.general.sounds.agent.title": "Agent",
|
||||
"settings.general.sounds.agent.description": "Speel geluid af wanneer de agent klaar is of aandacht nodig heeft",
|
||||
"settings.general.sounds.permissions.title": "Machtigingen",
|
||||
"settings.general.sounds.permissions.description": "Speel geluid af wanneer een machtiging vereist is",
|
||||
"settings.general.sounds.errors.title": "Fouten",
|
||||
"settings.general.sounds.errors.description": "Speel geluid af wanneer er een fout optreedt",
|
||||
|
||||
"settings.shortcuts.title": "Sneltoetsen",
|
||||
"settings.shortcuts.reset.button": "Terugzetten naar standaard",
|
||||
"settings.shortcuts.reset.toast.title": "Sneltoetsen gereset",
|
||||
"settings.shortcuts.reset.toast.description": "Sneltoetsen zijn teruggezet naar de standaardwaarden.",
|
||||
"settings.shortcuts.conflict.title": "Sneltoets al in gebruik",
|
||||
"settings.shortcuts.conflict.description": "{{keybind}} is al toegewezen aan {{titles}}.",
|
||||
"settings.shortcuts.unassigned": "Niet toegewezen",
|
||||
"settings.shortcuts.pressKeys": "Druk op toetsen",
|
||||
"settings.shortcuts.search.placeholder": "Zoek sneltoetsen",
|
||||
"settings.shortcuts.search.empty": "Geen sneltoetsen gevonden",
|
||||
|
||||
"settings.shortcuts.group.general": "Algemeen",
|
||||
"settings.shortcuts.group.session": "Sessie",
|
||||
"settings.shortcuts.group.navigation": "Navigatie",
|
||||
"settings.shortcuts.group.modelAndAgent": "Model en agent",
|
||||
"settings.shortcuts.group.terminal": "Terminal",
|
||||
"settings.shortcuts.group.prompt": "Prompt",
|
||||
|
||||
"settings.providers.title": "Providers",
|
||||
"settings.providers.description": "Provider-instellingen kunnen hier worden geconfigureerd.",
|
||||
"settings.providers.section.connected": "Verbonden providers",
|
||||
"settings.providers.connected.empty": "Geen verbonden providers",
|
||||
"settings.providers.connected.environmentDescription": "Verbonden vanuit uw omgevingsvariabelen",
|
||||
"settings.providers.section.popular": "Populaire providers",
|
||||
"settings.providers.custom.description": "Voeg een OpenAI-compatibele provider toe via basis-URL.",
|
||||
"settings.providers.tag.environment": "Omgeving",
|
||||
"settings.providers.tag.config": "Config",
|
||||
"settings.providers.tag.custom": "Aangepast",
|
||||
"settings.providers.tag.other": "Overig",
|
||||
"settings.models.title": "Modellen",
|
||||
"settings.models.description": "Model-instellingen kunnen hier worden geconfigureerd.",
|
||||
"settings.agents.title": "Agenten",
|
||||
"settings.agents.description": "Agent-instellingen kunnen hier worden geconfigureerd.",
|
||||
"settings.commands.title": "Opdrachten",
|
||||
"settings.commands.description": "Opdracht-instellingen kunnen hier worden geconfigureerd.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP-instellingen kunnen hier worden geconfigureerd.",
|
||||
|
||||
"settings.permissions.title": "Machtigingen",
|
||||
"settings.permissions.description": "Bepaal welke tools de server standaard mag gebruiken.",
|
||||
"settings.permissions.section.tools": "Tools",
|
||||
"settings.permissions.toast.updateFailed.title": "Bijwerken van machtigingen mislukt",
|
||||
|
||||
"settings.permissions.action.allow": "Toestaan",
|
||||
"settings.permissions.action.ask": "Vragen",
|
||||
"settings.permissions.action.deny": "Weigeren",
|
||||
|
||||
"settings.permissions.tool.read.title": "Lezen (Read)",
|
||||
"settings.permissions.tool.read.description": "Een bestand lezen (komt overeen met het bestandspad)",
|
||||
"settings.permissions.tool.edit.title": "Bewerken (Edit)",
|
||||
"settings.permissions.tool.edit.description":
|
||||
"Bestanden wijzigen, inclusief bewerkingen, wegschrijven, patches en multi-edits",
|
||||
"settings.permissions.tool.glob.title": "Glob",
|
||||
"settings.permissions.tool.glob.description": "Bestanden matchen met glob-patronen",
|
||||
"settings.permissions.tool.grep.title": "Grep",
|
||||
"settings.permissions.tool.grep.description": "Bestandsinhoud doorzoeken met behulp van reguliere expressies",
|
||||
"settings.permissions.tool.list.title": "Lijst (List)",
|
||||
"settings.permissions.tool.list.description": "Bestanden in een map weergeven",
|
||||
"settings.permissions.tool.bash.title": "Bash",
|
||||
"settings.permissions.tool.bash.description": "Shell-opdrachten uitvoeren",
|
||||
"settings.permissions.tool.task.title": "Taak (Task)",
|
||||
"settings.permissions.tool.task.description": "Sub-agenten lanceren",
|
||||
"settings.permissions.tool.skill.title": "Skill",
|
||||
"settings.permissions.tool.skill.description": "Een skill laden op naam",
|
||||
"settings.permissions.tool.lsp.title": "LSP",
|
||||
"settings.permissions.tool.lsp.description": "Language server query's uitvoeren",
|
||||
"settings.permissions.tool.todoread.title": "To-do lezen (Todo Read)",
|
||||
"settings.permissions.tool.todoread.description": "De to-do lijst lezen",
|
||||
"settings.permissions.tool.todowrite.title": "To-do schrijven (Todo Write)",
|
||||
"settings.permissions.tool.todowrite.description": "De to-do lijst bijwerken",
|
||||
"settings.permissions.tool.webfetch.title": "Web ophalen (Web Fetch)",
|
||||
"settings.permissions.tool.webfetch.description": "Inhoud ophalen van een URL",
|
||||
"settings.permissions.tool.websearch.title": "Web zoeken (Web Search)",
|
||||
"settings.permissions.tool.websearch.description": "Het web doorzoeken",
|
||||
"settings.permissions.tool.codesearch.title": "Code zoeken (Code Search)",
|
||||
"settings.permissions.tool.codesearch.description": "Zoeken naar code op het web",
|
||||
"settings.permissions.tool.external_directory.title": "Externe Map",
|
||||
"settings.permissions.tool.external_directory.description": "Toegang tot bestanden buiten de projectmap",
|
||||
"settings.permissions.tool.doom_loop.title": "Eindeloze Loop (Doom Loop)",
|
||||
"settings.permissions.tool.doom_loop.description": "Gedetecteerde herhaalde tool-aanroepen met identieke invoer",
|
||||
|
||||
"session.delete.failed.title": "Verwijderen van sessie mislukt",
|
||||
"session.delete.title": "Sessie verwijderen",
|
||||
"session.delete.confirm": 'Sessie "{{name}}" verwijderen?',
|
||||
"session.delete.button": "Sessie verwijderen",
|
||||
|
||||
"workspace.new": "Nieuwe werkruimte",
|
||||
"workspace.type.local": "lokaal",
|
||||
"workspace.type.sandbox": "sandbox",
|
||||
"workspace.create.failed.title": "Maken van werkruimte mislukt",
|
||||
"workspace.delete.failed.title": "Verwijderen van werkruimte mislukt",
|
||||
"workspace.resetting.title": "Werkruimte resetten",
|
||||
"workspace.resetting.description": "Dit kan een minuut duren.",
|
||||
"workspace.reset.failed.title": "Resetten van werkruimte mislukt",
|
||||
"workspace.reset.success.title": "Werkruimte gereset",
|
||||
"workspace.reset.success.description": "Werkruimte komt nu overeen met de standaardbranch.",
|
||||
"workspace.error.stillPreparing": "Werkruimte is nog aan het voorbereiden",
|
||||
"workspace.status.checking": "Controleren op ongemergde wijzigingen...",
|
||||
"workspace.status.error": "Kan git-status niet verifiëren.",
|
||||
"workspace.status.clean": "Geen ongemergde wijzigingen gedetecteerd.",
|
||||
"workspace.status.dirty": "Ongemergde wijzigingen gedetecteerd in deze werkruimte.",
|
||||
"workspace.delete.title": "Werkruimte verwijderen",
|
||||
"workspace.delete.confirm": 'Werkruimte "{{name}}" verwijderen?',
|
||||
"workspace.delete.button": "Werkruimte verwijderen",
|
||||
"workspace.reset.title": "Werkruimte resetten",
|
||||
"workspace.reset.confirm": 'Werkruimte "{{name}}" resetten?',
|
||||
"workspace.reset.button": "Werkruimte resetten",
|
||||
"workspace.reset.archived.none": "Geen actieve sessies zullen worden gearchiveerd.",
|
||||
"workspace.reset.archived.one": "1 sessie zal worden gearchiveerd.",
|
||||
"workspace.reset.archived.many": "{{count}} sessies zullen worden gearchiveerd.",
|
||||
"workspace.reset.note": "Dit zal de werkruimte resetten zodat deze overeenkomt met de standaardbranch.",
|
||||
}
|
||||
Generated
+1
-9
@@ -116,7 +116,6 @@ export const dict = {
|
||||
"dialog.model.empty": "Ingen modellresultater",
|
||||
"dialog.model.manage": "Administrer modeller",
|
||||
"dialog.model.manage.description": "Tilpass hvilke modeller som vises i modellvelgeren.",
|
||||
"dialog.model.manage.provider.toggle": "Veksle alle {{provider}}-modeller",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Gratis modeller levert av Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører",
|
||||
@@ -501,7 +500,6 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Laster endringer...",
|
||||
"session.review.empty": "Ingen endringer i denne sesjonen ennå",
|
||||
"session.review.noChanges": "Ingen endringer",
|
||||
"session.review.noVcs": "Ingen git VCS oppdaget, så øktendringer vil ikke bli oppdaget",
|
||||
|
||||
"session.files.selectToOpen": "Velg en fil å åpne",
|
||||
"session.files.all": "Alle filer",
|
||||
@@ -518,11 +516,7 @@ export const dict = {
|
||||
"session.todo.collapse": "Skjul",
|
||||
"session.todo.expand": "Utvid",
|
||||
|
||||
"session.modeSwitch.switching": "Bytter til {{mode}}-modus…",
|
||||
"session.modeSwitch.waiting": "Venter på at gjeldende oppgave er ferdig",
|
||||
"session.modeSwitch.notAvailable": "Agent ikke tilgjengelig",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" ikke funnet, bruker "{{actual}}"',
|
||||
|
||||
"session.new.title": "Bygg hva som helst",
|
||||
"session.new.worktree.main": "Hovedgren",
|
||||
"session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})",
|
||||
"session.new.worktree.create": "Opprett nytt worktree",
|
||||
@@ -622,8 +616,6 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Tilpass hvordan Kilo er tematisert.",
|
||||
"settings.general.row.font.title": "Skrift",
|
||||
"settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker",
|
||||
"settings.general.row.reasoningSummaries.title": "Vis resonneringssammendrag",
|
||||
"settings.general.row.reasoningSummaries.description": "Vis modellressonneringssammendrag i tidslinjen",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Utvid shell-verktøydeler",
|
||||
"settings.general.row.shellToolPartsExpanded.description": "Vis shell-verktøydeler utvidet som standard i tidslinjen",
|
||||
|
||||
Generated
+1
@@ -458,6 +458,7 @@ export const dict = {
|
||||
"session.todo.title": "Zadania",
|
||||
"session.todo.collapse": "Zwiń",
|
||||
"session.todo.expand": "Rozwiń",
|
||||
"session.new.title": "Zbuduj cokolwiek",
|
||||
"session.new.worktree.main": "Główna gałąź",
|
||||
"session.new.worktree.mainWithBranch": "Główna gałąź ({{branch}})",
|
||||
"session.new.worktree.create": "Utwórz nowe drzewo robocze",
|
||||
|
||||
Generated
+1
-9
@@ -113,7 +113,6 @@ export const dict = {
|
||||
"dialog.model.empty": "Модели не найдены",
|
||||
"dialog.model.manage": "Управление моделями",
|
||||
"dialog.model.manage.description": "Настройте какие модели появляются в выборе модели",
|
||||
"dialog.model.manage.provider.toggle": "Переключить все модели {{provider}}",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Бесплатные модели от Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров",
|
||||
@@ -501,7 +500,6 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Загрузка изменений...",
|
||||
"session.review.empty": "Изменений в этой сессии пока нет",
|
||||
"session.review.noChanges": "Нет изменений",
|
||||
"session.review.noVcs": "Git VCS не обнаружен, поэтому изменения сеанса не будут отслеживаться",
|
||||
"session.files.selectToOpen": "Выберите файл, чтобы открыть",
|
||||
"session.files.all": "Все файлы",
|
||||
"session.files.binaryContent": "Двоичный файл (содержимое не может быть отображено)",
|
||||
@@ -516,11 +514,7 @@ export const dict = {
|
||||
"session.todo.collapse": "Свернуть",
|
||||
"session.todo.expand": "Развернуть",
|
||||
|
||||
"session.modeSwitch.switching": "Переключение в режим {{mode}}…",
|
||||
"session.modeSwitch.waiting": "Ожидание завершения текущей задачи",
|
||||
"session.modeSwitch.notAvailable": "Агент недоступен",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" не найден, используется "{{actual}}"',
|
||||
|
||||
"session.new.title": "Создавайте что угодно",
|
||||
"session.new.worktree.main": "Основная ветка",
|
||||
"session.new.worktree.mainWithBranch": "Основная ветка ({{branch}})",
|
||||
"session.new.worktree.create": "Создать новый worktree",
|
||||
@@ -620,8 +614,6 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Настройте оформление Kilo.",
|
||||
"settings.general.row.font.title": "Шрифт",
|
||||
"settings.general.row.font.description": "Настройте моноширинный шрифт для блоков кода",
|
||||
"settings.general.row.reasoningSummaries.title": "Показывать сводки рассуждений",
|
||||
"settings.general.row.reasoningSummaries.description": "Отображать сводки рассуждений модели в хронологии",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Разворачивать элементы инструмента shell",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
|
||||
Generated
+1
-9
@@ -113,7 +113,6 @@ export const dict = {
|
||||
"dialog.model.empty": "ไม่พบผลลัพธ์โมเดล",
|
||||
"dialog.model.manage": "จัดการโมเดล",
|
||||
"dialog.model.manage.description": "ปรับแต่งโมเดลที่จะปรากฏในตัวเลือกโมเดล",
|
||||
"dialog.model.manage.provider.toggle": "สลับโมเดล {{provider}} ทั้งหมด",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "โมเดลฟรีที่จัดหาให้โดย Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "เพิ่มโมเดลเพิ่มเติมจากผู้ให้บริการยอดนิยม",
|
||||
@@ -495,7 +494,6 @@ export const dict = {
|
||||
"session.review.loadingChanges": "กำลังโหลดการเปลี่ยนแปลง...",
|
||||
"session.review.empty": "ยังไม่มีการเปลี่ยนแปลงในเซสชันนี้",
|
||||
"session.review.noChanges": "ไม่มีการเปลี่ยนแปลง",
|
||||
"session.review.noVcs": "ไม่พบ git VCS ดังนั้นจะไม่สามารถตรวจจับการเปลี่ยนแปลงของเซสชันได้",
|
||||
|
||||
"session.files.selectToOpen": "เลือกไฟล์เพื่อเปิด",
|
||||
"session.files.all": "ไฟล์ทั้งหมด",
|
||||
@@ -512,11 +510,7 @@ export const dict = {
|
||||
"session.todo.collapse": "ย่อ",
|
||||
"session.todo.expand": "ขยาย",
|
||||
|
||||
"session.modeSwitch.switching": "กำลังสลับไปยังโหมด {{mode}}…",
|
||||
"session.modeSwitch.waiting": "รอให้งานปัจจุบันเสร็จสมบูรณ์",
|
||||
"session.modeSwitch.notAvailable": "ตัวแทนไม่พร้อมใช้งาน",
|
||||
"session.modeSwitch.fallback": 'ไม่พบ "{{requested}}" กำลังใช้ "{{actual}}"',
|
||||
|
||||
"session.new.title": "สร้างอะไรก็ได้",
|
||||
"session.new.worktree.main": "สาขาหลัก",
|
||||
"session.new.worktree.mainWithBranch": "สาขาหลัก ({{branch}})",
|
||||
"session.new.worktree.create": "สร้าง worktree ใหม่",
|
||||
@@ -613,8 +607,6 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ Kilo มีธีม",
|
||||
"settings.general.row.font.title": "ฟอนต์",
|
||||
"settings.general.row.font.description": "ปรับแต่งฟอนต์โมโนที่ใช้ในบล็อกโค้ด",
|
||||
"settings.general.row.reasoningSummaries.title": "แสดงสรุปการให้เหตุผล",
|
||||
"settings.general.row.reasoningSummaries.description": "แสดงสรุปการให้เหตุผลของโมเดลในไทม์ไลน์",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "ขยายส่วนเครื่องมือ shell",
|
||||
"settings.general.row.shellToolPartsExpanded.description": "แสดงส่วนเครื่องมือ shell แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
|
||||
|
||||
Generated
+1
@@ -522,6 +522,7 @@ export const dict = {
|
||||
"session.todo.collapse": "Daralt",
|
||||
"session.todo.expand": "Genişlet",
|
||||
|
||||
"session.new.title": "İstediğini yap",
|
||||
"session.new.worktree.main": "Ana dal",
|
||||
"session.new.worktree.mainWithBranch": "Ana dal ({{branch}})",
|
||||
"session.new.worktree.create": "Yeni çalışma ağacı oluştur",
|
||||
|
||||
Generated
+1
@@ -510,6 +510,7 @@ export const dict = {
|
||||
"session.todo.title": "待办事项",
|
||||
"session.todo.collapse": "折叠",
|
||||
"session.todo.expand": "展开",
|
||||
"session.new.title": "构建任何东西",
|
||||
"session.new.worktree.main": "主分支",
|
||||
"session.new.worktree.mainWithBranch": "主分支({{branch}})",
|
||||
"session.new.worktree.create": "创建新的 worktree",
|
||||
|
||||
Generated
+1
-9
@@ -117,7 +117,6 @@ export const dict = {
|
||||
"dialog.model.empty": "找不到模型",
|
||||
"dialog.model.manage": "管理模型",
|
||||
"dialog.model.manage.description": "自訂模型選擇器中顯示的模型。",
|
||||
"dialog.model.manage.provider.toggle": "切換所有 {{provider}} 模型",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Kilo 提供的免費模型",
|
||||
"dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型",
|
||||
@@ -493,7 +492,6 @@ export const dict = {
|
||||
"session.review.loadingChanges": "正在載入變更...",
|
||||
"session.review.empty": "此工作階段暫無變更",
|
||||
"session.review.noChanges": "沒有變更",
|
||||
"session.review.noVcs": "未偵測到 git VCS,因此無法偵測到工作階段變更",
|
||||
"session.files.selectToOpen": "選取要開啟的檔案",
|
||||
"session.files.all": "所有檔案",
|
||||
"session.files.binaryContent": "二進位檔案(無法顯示內容)",
|
||||
@@ -508,11 +506,7 @@ export const dict = {
|
||||
"session.todo.collapse": "折疊",
|
||||
"session.todo.expand": "展開",
|
||||
|
||||
"session.modeSwitch.switching": "正在切換到 {{mode}} 模式…",
|
||||
"session.modeSwitch.waiting": "等待目前任務完成",
|
||||
"session.modeSwitch.notAvailable": "Agent 不可用",
|
||||
"session.modeSwitch.fallback": '找不到 "{{requested}}",使用 "{{actual}}"',
|
||||
|
||||
"session.new.title": "建構任何東西",
|
||||
"session.new.worktree.main": "主分支",
|
||||
"session.new.worktree.mainWithBranch": "主分支 ({{branch}})",
|
||||
"session.new.worktree.create": "建立新的 worktree",
|
||||
@@ -608,8 +602,6 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "自訂 Kilo 的主題。",
|
||||
"settings.general.row.font.title": "字型",
|
||||
"settings.general.row.font.description": "自訂程式碼區塊使用的等寬字型",
|
||||
"settings.general.row.reasoningSummaries.title": "顯示推理摘要",
|
||||
"settings.general.row.reasoningSummaries.description": "在時間軸中顯示模型推理摘要",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "展開 shell 工具區塊",
|
||||
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
|
||||
|
||||
@@ -1 +1,29 @@
|
||||
@import "@kilocode/kilo-ui/styles/tailwind"; /* kilocode_change */
|
||||
@import "@opencode-ai/ui/styles/tailwind";
|
||||
|
||||
@layer components {
|
||||
[data-component="getting-started"] {
|
||||
container-type: inline-size;
|
||||
container-name: getting-started;
|
||||
}
|
||||
|
||||
[data-component="getting-started-actions"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem; /* gap-3 */
|
||||
}
|
||||
|
||||
[data-component="getting-started-actions"] > [data-component="button"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@container getting-started (min-width: 17rem) {
|
||||
[data-component="getting-started-actions"] {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-component="getting-started-actions"] > [data-component="button"] {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user