Compare commits
163
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
440eb76e52 | ||
|
|
b551195818 | ||
|
|
d7c2d5db3b | ||
|
|
1dd88aeae6 | ||
|
|
2875405514 | ||
|
|
8c0300c021 | ||
|
|
26b786dd3f | ||
|
|
afce869d3b | ||
|
|
b738d88ec4 | ||
|
|
83646e0366 | ||
|
|
c40ce47e92 | ||
|
|
b1c44c7e5c | ||
|
|
081f065942 | ||
|
|
8ddef975b7 | ||
|
|
2f78705f6e | ||
|
|
a0bc656215 | ||
|
|
47f00d23b3 | ||
|
|
40ebc34909 | ||
|
|
e08705f4ef | ||
|
|
7c748ef089 | ||
|
|
fba5a79c45 | ||
|
|
dbde377ab0 | ||
|
|
9adcf524e2 | ||
|
|
531b1941a0 | ||
|
|
a1c46e05ee | ||
|
|
1fe1457cfa | ||
|
|
aedd85d885 | ||
|
|
5b3d94ebaa | ||
|
|
1a6a3f4b54 | ||
|
|
3116cfc167 | ||
|
|
05529f66d7 | ||
|
|
ef09dddaa5 | ||
|
|
bf7af99a3f | ||
|
|
7555742bf0 | ||
|
|
fa20bc296b | ||
|
|
195731f347 | ||
|
|
72de9fe7a6 | ||
|
|
bec1148192 | ||
|
|
8c8d888140 | ||
|
|
d3a247bfff | ||
|
|
9ef319f25f | ||
|
|
64e2bf8bf0 | ||
|
|
556adad67b | ||
|
|
843bbc973a | ||
|
|
173804c097 | ||
|
|
4086a9ae8e | ||
|
|
2614342f97 | ||
|
|
4387f9fb9a | ||
|
|
31e2feb347 | ||
|
|
2896b8a863 | ||
|
|
0d38e69038 | ||
|
|
9679e0c59c | ||
|
|
41bc4ec7f0 | ||
|
|
912098928a | ||
|
|
222bddc41a | ||
|
|
9436cb575b | ||
|
|
d1686661c0 | ||
|
|
305007aa0c | ||
|
|
a2c28fc8d7 | ||
|
|
ce87121067 | ||
|
|
ecd7854853 | ||
|
|
57b8c62909 | ||
|
|
28dc5de6a8 | ||
|
|
c875a1fc90 | ||
|
|
1721c6efdf | ||
|
|
93592702c3 | ||
|
|
61d3f788b8 | ||
|
|
a3b281b2f3 | ||
|
|
c8622df762 | ||
|
|
c277ee8cbf | ||
|
|
a2face30f4 | ||
|
|
a219615fe5 | ||
|
|
af06175b1f | ||
|
|
2e8d8de58b | ||
|
|
310de8b1ea | ||
|
|
891875402c | ||
|
|
154cbf6996 | ||
|
|
64bafce665 | ||
|
|
5588453cbe | ||
|
|
5aaf8f8247 | ||
|
|
8c1f1f13dc | ||
|
|
b942e0b4dc | ||
|
|
f282613746 | ||
|
|
7c440ae82c | ||
|
|
b7bd561eaa | ||
|
|
6daa962aaa | ||
|
|
93a07e5a2a | ||
|
|
93e060272a | ||
|
|
acac05f22e | ||
|
|
b5a4671c64 | ||
|
|
a68fedd4a6 | ||
|
|
015cd404e4 | ||
|
|
9921809565 | ||
|
|
137336f373 | ||
|
|
d940d17918 | ||
|
|
0a5d5bc524 | ||
|
|
a30696f9bf | ||
|
|
25bdd77b1d | ||
|
|
2f12e8ee92 | ||
|
|
95211a8854 | ||
|
|
6b5cf936a2 | ||
|
|
17e62b050f | ||
|
|
ee84eb44ee | ||
|
|
82dd4b6908 | ||
|
|
185858749b | ||
|
|
39a504773c | ||
|
|
b7b734f51f | ||
|
|
dcff5b6596 | ||
|
|
4f7da2b757 | ||
|
|
60e616ec81 | ||
|
|
416964acd0 | ||
|
|
017ad2c7f7 | ||
|
|
e33eb1b058 | ||
|
|
857b8a4b56 | ||
|
|
3975329629 | ||
|
|
54e14c1a17 | ||
|
|
3741516fe3 | ||
|
|
76381f33d5 | ||
|
|
95d0d476e3 | ||
|
|
7508839b70 | ||
|
|
e88cbefabe | ||
|
|
a38bae684f | ||
|
|
08671e3155 | ||
|
|
0d557721cf | ||
|
|
e709808b32 | ||
|
|
0d22068c90 | ||
|
|
d116c227e0 | ||
|
|
3f07dffbb0 | ||
|
|
801e4a8a9d | ||
|
|
3adeed8f97 | ||
|
|
1275c71a63 | ||
|
|
ca8c23dd71 | ||
|
|
acc2bf5db9 | ||
|
|
96fbc30945 | ||
|
|
ba545ba9b3 | ||
|
|
188cc24bfc | ||
|
|
5e3162b7f4 | ||
|
|
f9aa209131 | ||
|
|
f86f654cda | ||
|
|
aadd2e13d7 | ||
|
|
531357b40c | ||
|
|
aa6b552c39 | ||
|
|
a3f1918489 | ||
|
|
a9fca05d8b | ||
|
|
824165eb79 | ||
|
|
562c9d76d9 | ||
|
|
c002ca03ba | ||
|
|
befb5d54fb | ||
|
|
69f5f657f2 | ||
|
|
0405b425f5 | ||
|
|
70cf609ce9 | ||
|
|
2f76b49df3 | ||
|
|
dfd5f38408 | ||
|
|
3b93e8d95c | ||
|
|
23631a9393 | ||
|
|
f1e0c31b8f | ||
|
|
30a25e4edc | ||
|
|
ea1aba4192 | ||
|
|
b9aad20be6 | ||
|
|
965f32ad63 | ||
|
|
cf828fff85 | ||
|
|
cf8b033be1 | ||
|
|
1bd5dc5382 |
@@ -1,4 +1,5 @@
|
||||
# web + desktop packages
|
||||
packages/app/ @adamdotdevin
|
||||
packages/tauri/ @adamdotdevin
|
||||
packages/desktop/src-tauri/ @brendonovich
|
||||
packages/desktop/ @adamdotdevin
|
||||
|
||||
@@ -18,6 +18,7 @@ permissions:
|
||||
jobs:
|
||||
close-stale-prs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Close inactive PRs
|
||||
uses: actions/github-script@v8
|
||||
@@ -25,6 +26,15 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const DAYS_INACTIVE = 60
|
||||
const MAX_RETRIES = 3
|
||||
|
||||
// Adaptive delay: fast for small batches, slower for large to respect
|
||||
// GitHub's 80 content-generating requests/minute limit
|
||||
const SMALL_BATCH_THRESHOLD = 10
|
||||
const SMALL_BATCH_DELAY_MS = 1000 // 1s for daily operations (≤10 PRs)
|
||||
const LARGE_BATCH_DELAY_MS = 2000 // 2s for backlog (>10 PRs) = ~30 ops/min, well under 80 limit
|
||||
|
||||
const startTime = Date.now()
|
||||
const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000)
|
||||
const { owner, repo } = context.repo
|
||||
const dryRun = context.payload.inputs?.dryRun === "true"
|
||||
@@ -32,6 +42,42 @@ jobs:
|
||||
core.info(`Dry run mode: ${dryRun}`)
|
||||
core.info(`Cutoff date: ${cutoff.toISOString()}`)
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async function withRetry(fn, description = 'API call') {
|
||||
let lastError
|
||||
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const result = await fn()
|
||||
return result
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
const isRateLimited = error.status === 403 &&
|
||||
(error.message?.includes('rate limit') || error.message?.includes('secondary'))
|
||||
|
||||
if (!isRateLimited) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// Parse retry-after header, default to 60 seconds
|
||||
const retryAfter = error.response?.headers?.['retry-after']
|
||||
? parseInt(error.response.headers['retry-after'])
|
||||
: 60
|
||||
|
||||
// Exponential backoff: retryAfter * 2^attempt
|
||||
const backoffMs = retryAfter * 1000 * Math.pow(2, attempt)
|
||||
|
||||
core.warning(`${description}: Rate limited (attempt ${attempt + 1}/${MAX_RETRIES}). Waiting ${backoffMs / 1000}s before retry...`)
|
||||
|
||||
await sleep(backoffMs)
|
||||
}
|
||||
}
|
||||
core.error(`${description}: Max retries (${MAX_RETRIES}) exceeded`)
|
||||
throw lastError
|
||||
}
|
||||
|
||||
const query = `
|
||||
query($owner: String!, $repo: String!, $cursor: String) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
@@ -73,17 +119,27 @@ jobs:
|
||||
const allPrs = []
|
||||
let cursor = null
|
||||
let hasNextPage = true
|
||||
let pageCount = 0
|
||||
|
||||
while (hasNextPage) {
|
||||
const result = await github.graphql(query, {
|
||||
owner,
|
||||
repo,
|
||||
cursor,
|
||||
})
|
||||
pageCount++
|
||||
core.info(`Fetching page ${pageCount} of open PRs...`)
|
||||
|
||||
const result = await withRetry(
|
||||
() => github.graphql(query, { owner, repo, cursor }),
|
||||
`GraphQL page ${pageCount}`
|
||||
)
|
||||
|
||||
allPrs.push(...result.repository.pullRequests.nodes)
|
||||
hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage
|
||||
cursor = result.repository.pullRequests.pageInfo.endCursor
|
||||
|
||||
core.info(`Page ${pageCount}: fetched ${result.repository.pullRequests.nodes.length} PRs (total: ${allPrs.length})`)
|
||||
|
||||
// Delay between pagination requests (use small batch delay for reads)
|
||||
if (hasNextPage) {
|
||||
await sleep(SMALL_BATCH_DELAY_MS)
|
||||
}
|
||||
}
|
||||
|
||||
core.info(`Found ${allPrs.length} open pull requests`)
|
||||
@@ -114,28 +170,66 @@ jobs:
|
||||
|
||||
core.info(`Found ${stalePrs.length} stale pull requests`)
|
||||
|
||||
// ============================================
|
||||
// Close stale PRs
|
||||
// ============================================
|
||||
const requestDelayMs = stalePrs.length > SMALL_BATCH_THRESHOLD
|
||||
? LARGE_BATCH_DELAY_MS
|
||||
: SMALL_BATCH_DELAY_MS
|
||||
|
||||
core.info(`Using ${requestDelayMs}ms delay between operations (${stalePrs.length > SMALL_BATCH_THRESHOLD ? 'large' : 'small'} batch mode)`)
|
||||
|
||||
let closedCount = 0
|
||||
let skippedCount = 0
|
||||
|
||||
for (const pr of stalePrs) {
|
||||
const issue_number = pr.number
|
||||
const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.`
|
||||
|
||||
if (dryRun) {
|
||||
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.author.login}: ${pr.title}`)
|
||||
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`)
|
||||
continue
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body: closeComment,
|
||||
})
|
||||
try {
|
||||
// Add comment
|
||||
await withRetry(
|
||||
() => github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body: closeComment,
|
||||
}),
|
||||
`Comment on PR #${issue_number}`
|
||||
)
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue_number,
|
||||
state: "closed",
|
||||
})
|
||||
// Close PR
|
||||
await withRetry(
|
||||
() => github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue_number,
|
||||
state: "closed",
|
||||
}),
|
||||
`Close PR #${issue_number}`
|
||||
)
|
||||
|
||||
core.info(`Closed PR #${issue_number} from ${pr.author.login}: ${pr.title}`)
|
||||
closedCount++
|
||||
core.info(`Closed PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`)
|
||||
|
||||
// Delay before processing next PR
|
||||
await sleep(requestDelayMs)
|
||||
} catch (error) {
|
||||
skippedCount++
|
||||
core.error(`Failed to close PR #${issue_number}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
||||
core.info(`\n========== Summary ==========`)
|
||||
core.info(`Total open PRs found: ${allPrs.length}`)
|
||||
core.info(`Stale PRs identified: ${stalePrs.length}`)
|
||||
core.info(`PRs closed: ${closedCount}`)
|
||||
core.info(`PRs skipped (errors): ${skippedCount}`)
|
||||
core.info(`Elapsed time: ${elapsed}s`)
|
||||
core.info(`=============================`)
|
||||
|
||||
@@ -5,6 +5,7 @@ node_modules
|
||||
.env
|
||||
.idea
|
||||
.vscode
|
||||
.codex
|
||||
*~
|
||||
playground
|
||||
tmp
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
// "plugin": ["opencode-openai-codex-auth"],
|
||||
// "enterprise": {
|
||||
// "url": "https://enterprise.dev.opencode.ai",
|
||||
// },
|
||||
@@ -9,12 +8,7 @@
|
||||
"options": {},
|
||||
},
|
||||
},
|
||||
"mcp": {
|
||||
"context7": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.context7.com/mcp",
|
||||
},
|
||||
},
|
||||
"mcp": {},
|
||||
"tools": {
|
||||
"github-triage": false,
|
||||
"github-pr-search": false,
|
||||
|
||||
@@ -82,7 +82,7 @@ The install script respects the following priority order for the installation pa
|
||||
|
||||
1. `$OPENCODE_INSTALL_DIR` - Custom installation directory
|
||||
2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path
|
||||
3. `$HOME/bin` - Standard user binary directory (if exists or can be created)
|
||||
3. `$HOME/bin` - Standard user binary directory (if it exists or can be created)
|
||||
4. `$HOME/.opencode/bin` - Default fallback
|
||||
|
||||
```bash
|
||||
@@ -95,20 +95,20 @@ XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
OpenCode includes two built-in agents you can switch between with the `Tab` key.
|
||||
|
||||
- **build** - Default, full access agent for development work
|
||||
- **build** - Default, full-access agent for development work
|
||||
- **plan** - Read-only agent for analysis and code exploration
|
||||
- Denies file edits by default
|
||||
- Asks permission before running bash commands
|
||||
- Ideal for exploring unfamiliar codebases or planning changes
|
||||
|
||||
Also, included is a **general** subagent for complex searches and multistep tasks.
|
||||
Also included is a **general** subagent for complex searches and multistep tasks.
|
||||
This is used internally and can be invoked using `@general` in messages.
|
||||
|
||||
Learn more about [agents](https://opencode.ai/docs/agents).
|
||||
|
||||
### Documentation
|
||||
|
||||
For more info on how to configure OpenCode [**head over to our docs**](https://opencode.ai/docs).
|
||||
For more info on how to configure OpenCode, [**head over to our docs**](https://opencode.ai/docs).
|
||||
|
||||
### Contributing
|
||||
|
||||
@@ -116,7 +116,7 @@ If you're interested in contributing to OpenCode, please read our [contributing
|
||||
|
||||
### Building on OpenCode
|
||||
|
||||
If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in any way.
|
||||
If you are working on a project that's related to OpenCode and is using "opencode" as part of its name, for example "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in any way.
|
||||
|
||||
### FAQ
|
||||
|
||||
@@ -125,10 +125,10 @@ If you are working on a project that's related to OpenCode and is using "opencod
|
||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||
|
||||
- 100% open source
|
||||
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen); OpenCode can be used with Claude, OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
|
||||
- Out of the box LSP support
|
||||
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important.
|
||||
- Out-of-the-box LSP support
|
||||
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||
- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
||||
- A client/server architecture. This, for example, can allow OpenCode to run on your computer while you drive it remotely from a mobile app, meaning that the TUI frontend is just one of the possible clients.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -44,7 +44,7 @@
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"ghostty-web": "0.3.0",
|
||||
"ghostty-web": "0.4.0",
|
||||
"luxon": "catalog:",
|
||||
"marked": "catalog:",
|
||||
"marked-shiki": "catalog:",
|
||||
@@ -73,7 +73,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -107,7 +107,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -182,7 +182,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -213,7 +213,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -242,7 +242,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -258,14 +258,14 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.13.0",
|
||||
"@agentclientprotocol/sdk": "0.14.1",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.74",
|
||||
"@ai-sdk/anthropic": "2.0.58",
|
||||
"@ai-sdk/azure": "2.0.91",
|
||||
@@ -284,9 +284,10 @@
|
||||
"@ai-sdk/provider-utils": "3.0.20",
|
||||
"@ai-sdk/togetherai": "1.0.34",
|
||||
"@ai-sdk/vercel": "1.0.33",
|
||||
"@ai-sdk/xai": "2.0.56",
|
||||
"@ai-sdk/xai": "2.0.51",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@gitlab/gitlab-ai-provider": "3.3.1",
|
||||
"@gitlab/gitlab-ai-provider": "3.4.0",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.2",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.25.2",
|
||||
@@ -307,8 +308,9 @@
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"ai": "catalog:",
|
||||
"ai-gateway-provider": "2.3.1",
|
||||
"bonjour-service": "1.3.0",
|
||||
"bun-pty": "0.4.4",
|
||||
"bun-pty": "0.4.8",
|
||||
"chokidar": "4.0.3",
|
||||
"clipboardy": "4.0.0",
|
||||
"decimal.js": "10.5.0",
|
||||
@@ -362,7 +364,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -382,7 +384,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.90.10",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -393,7 +395,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -406,7 +408,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -448,7 +450,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -459,7 +461,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -494,9 +496,6 @@
|
||||
"web-tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
@@ -557,7 +556,7 @@
|
||||
|
||||
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
|
||||
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.13.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Z6/Fp4cXLbYdMXr5AK752JM5qG2VKb6ShM0Ql6FimBSckMmLyK54OA20UhPYoH4C37FSFwUTARuwQOwQUToYrw=="],
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.14.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.74", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-q83HE3FBb/HPIvjXsehrHOgCuGHPorSMFt6BYnzIYZy8gNnSqV1OWX4oXVsCAuYPPMtYW/KMK35hmoIFV8QKoQ=="],
|
||||
|
||||
@@ -569,8 +568,16 @@
|
||||
|
||||
"@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yJ9kP5cEDJwo8qpITq5TQFD8YNfNtW+HbyvWwrKMbFzmiMvIZuk95HIaFXE7PCTuZsqMA05yYu+qX/vQ3rNKjA=="],
|
||||
|
||||
"@ai-sdk/deepgram": ["@ai-sdk/deepgram@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-lqmINr+1Jy2yGXxnQB6IrC2xMtUY5uK96pyKfqTj1kLlXGatKnJfXF7WTkOGgQrFqIYqpjDz+sPVR3n0KUEUtA=="],
|
||||
|
||||
"@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hn2y8Q+2iZgGNVJyzPsH8EECECryFMVmxBJrBvBWoi8xcJPRyt0fZP5dOSLyGg3q0oxmPS9M0Eq0NNlKot/bYQ=="],
|
||||
|
||||
"@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NiKjvqXI/96e/7SjZGgQH141PBqggsF7fNbjGTv4RgVWayMXp9mj0Ou2NjAUGwwxJwj/qseY0gXiDCYaHWFBkw=="],
|
||||
|
||||
"@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4d5EKu0OW7Gf5WFpGo4ixn0iWEwA+GpteqUjEznWGmi7qdLE5zdkbRik5B1HrDDiw5P90yO51xBex/Fp50JcVA=="],
|
||||
|
||||
"@ai-sdk/fireworks": ["@ai-sdk/fireworks@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-WWOz5Kj+5fVe94h7WeReqjUOVtAquDE2kM575FUc8CsVxH2tRfA5cLa8nu3bknSezsKt3i67YM6mvCRxiXCkWA=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5Nrkj8B4MzkkOfjjA+Cs5pamkbkK4lI11bx80QV7TFcen/hWA8wEC+UVzwuM5H2zpekoNMjvl6GonHnR62XIZw=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="],
|
||||
@@ -595,7 +602,7 @@
|
||||
|
||||
"@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qwjm+HdwKasu7L9bDUryBMGKDMscIEzMUkjw/33uGdJpktzyNW13YaNIObOZ2HkskqDMIQJSd4Ao2BBT8fEYLw=="],
|
||||
|
||||
"@ai-sdk/xai": ["@ai-sdk/xai@2.0.56", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FGlqwWc3tAYqDHE8r8hQGQLcMiPUwgz90oU2QygUH930OWtCLapFkSu114DgVaIN/qoM1DUX+inv0Ee74Fgp5g=="],
|
||||
"@ai-sdk/xai": ["@ai-sdk/xai@2.0.51", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AI3le03qiegkZvn9hpnpDwez49lOvQLj4QUBT8H41SMbrdTYOxn3ktTwrsSu90cNDdzKGMvoH0u2GHju1EdnCg=="],
|
||||
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
@@ -911,8 +918,22 @@
|
||||
|
||||
"@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6" } }, "sha512-PBFa1wGyYzRExMDzBmAWC6/kdfG1oLn4pLpBeTfIRrALPjcGA/59HP3e7q9J0Smk4pC7U+lWkA2LHR8FYV8U7Q=="],
|
||||
|
||||
"@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
"@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
|
||||
|
||||
"@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="],
|
||||
|
||||
"@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
|
||||
|
||||
"@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
|
||||
|
||||
"@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="],
|
||||
|
||||
"@fastify/rate-limit": ["@fastify/rate-limit@10.3.0", "", { "dependencies": { "@lukeed/ms": "^2.0.2", "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
|
||||
@@ -925,7 +946,9 @@
|
||||
|
||||
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.3.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-J4/LfVcxOKbR2gfoBWRKp1BpWppprC2Cz/Ff5E0B/0lS341CDtZwzkgWvHfkM/XU6q83JRs059dS0cR8VOODOQ=="],
|
||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.4.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-1fEZgqjSZ0WLesftw/J5UtFuJCYFDvCZCHhTH5PZAmpDEmCwllJBoe84L3+vIk38V2FGDMTW128iKTB2mVzr3A=="],
|
||||
|
||||
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.2", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-pvGrC+aDVLY8bRCC/fZaG/Qihvt2r4by5xbTo5JTSz9O7yIcR6xG2d9Wkuu4bcXFz674z2C+i5bUk+J/RSdBpg=="],
|
||||
|
||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||
|
||||
@@ -1119,6 +1142,8 @@
|
||||
|
||||
"@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="],
|
||||
|
||||
"@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
|
||||
|
||||
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
|
||||
|
||||
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
||||
@@ -1359,6 +1384,8 @@
|
||||
|
||||
"@pierre/diffs": ["@pierre/diffs@1.0.2", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/transformers": "3.19.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.19.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-RkFSDD5X/U+8QjyilPViYGJfmJNWXR17zTL8zw48+DcVC1Ujbh6I1edyuRnFfgRzpft05x2DSCkz2cjoIAxPvQ=="],
|
||||
|
||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@planetscale/database": ["@planetscale/database@1.19.0", "", {}, "sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA=="],
|
||||
@@ -1935,6 +1962,8 @@
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
|
||||
|
||||
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
@@ -1949,6 +1978,8 @@
|
||||
|
||||
"ai": ["ai@5.0.124", "", { "dependencies": { "@ai-sdk/gateway": "2.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Li6Jw9F9qsvFJXZPBfxj38ddP2iURCnMs96f9Q3OeQzrDVcl1hvtwSEAuxA/qmfh6SDV2ERqFUOFzigvr0697g=="],
|
||||
|
||||
"ai-gateway-provider": ["ai-gateway-provider@2.3.1", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.19", "ai": "^5.0.116" }, "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^3.0.71", "@ai-sdk/anthropic": "^2.0.56", "@ai-sdk/azure": "^2.0.90", "@ai-sdk/cerebras": "^1.0.33", "@ai-sdk/cohere": "^2.0.21", "@ai-sdk/deepgram": "^1.0.21", "@ai-sdk/deepseek": "^1.0.32", "@ai-sdk/elevenlabs": "^1.0.21", "@ai-sdk/fireworks": "^1.0.30", "@ai-sdk/google": "^2.0.51", "@ai-sdk/google-vertex": "3.0.90", "@ai-sdk/groq": "^2.0.33", "@ai-sdk/mistral": "^2.0.26", "@ai-sdk/openai": "^2.0.88", "@ai-sdk/perplexity": "^2.0.22", "@ai-sdk/xai": "^2.0.42", "@openrouter/ai-sdk-provider": "^1.5.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.29" } }, "sha512-PqI6TVNEDNwr7kOhy7XUGnA8XJB1SpeA9aLqGjr0CyWkKgH+y+ofPm8MZGZ74DOwVejDF+POZq0Qs9jKEKUeYg=="],
|
||||
|
||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
@@ -2009,10 +2040,14 @@
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"avvio": ["avvio@9.1.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw=="],
|
||||
|
||||
"await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
|
||||
|
||||
"aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="],
|
||||
@@ -2097,7 +2132,7 @@
|
||||
|
||||
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.4", "", {}, "sha512-WK4G6uWsZgu1v4hKIlw6G1q2AOf8Rbga2Yr7RnxArVjjyb+mtVa/CFc9GOJf+OYSJSH8k7LonAtQOVeNAddRyg=="],
|
||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
||||
|
||||
@@ -2455,16 +2490,26 @@
|
||||
|
||||
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fast-json-stringify": ["fast-json-stringify@6.2.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-Eaf/KNIDwHkzfyeQFNfLXJnQ7cl1XQI3+zRqmPlvtkMigbXnAcasTrvJQmquBSxKfFGeRA6PFog8t+hFmpDoWw=="],
|
||||
|
||||
"fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="],
|
||||
|
||||
"fastify": ["fastify@5.7.4", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA=="],
|
||||
|
||||
"fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="],
|
||||
|
||||
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
@@ -2479,6 +2524,8 @@
|
||||
|
||||
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
|
||||
|
||||
"find-my-way": ["find-my-way@9.4.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w=="],
|
||||
|
||||
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||
|
||||
"finity": ["finity@0.5.4", "", {}, "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA=="],
|
||||
@@ -2555,7 +2602,7 @@
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||
|
||||
"ghostty-web": ["ghostty-web@0.3.0", "", {}, "sha512-SAdSHWYF20GMZUB0n8kh1N6Z4ljMnuUqT8iTB2n5FAPswEV10MejEpLlhW/769GL5+BQa1NYwEg9y/XCckV5+A=="],
|
||||
"ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="],
|
||||
|
||||
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
||||
|
||||
@@ -2845,6 +2892,8 @@
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
"json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="],
|
||||
|
||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
@@ -2881,6 +2930,8 @@
|
||||
|
||||
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
|
||||
|
||||
"light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||
@@ -3183,6 +3234,8 @@
|
||||
|
||||
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
|
||||
|
||||
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
@@ -3289,6 +3342,12 @@
|
||||
|
||||
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
|
||||
|
||||
"pino": ["pino@10.3.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA=="],
|
||||
|
||||
"pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
|
||||
|
||||
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
|
||||
|
||||
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
||||
|
||||
"pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="],
|
||||
@@ -3341,6 +3400,8 @@
|
||||
|
||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||
|
||||
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
|
||||
|
||||
"promise.allsettled": ["promise.allsettled@1.0.7", "", { "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" } }, "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA=="],
|
||||
|
||||
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
|
||||
@@ -3363,6 +3424,8 @@
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
||||
|
||||
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
@@ -3397,6 +3460,8 @@
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
||||
|
||||
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
|
||||
|
||||
"recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
|
||||
@@ -3463,6 +3528,8 @@
|
||||
|
||||
"restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="],
|
||||
|
||||
"ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="],
|
||||
|
||||
"retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="],
|
||||
|
||||
"retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="],
|
||||
@@ -3475,6 +3542,8 @@
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||
|
||||
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
||||
|
||||
"rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="],
|
||||
@@ -3497,6 +3566,10 @@
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"safe-regex2": ["safe-regex2@5.0.0", "", { "dependencies": { "ret": "~0.5.0" } }, "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw=="],
|
||||
|
||||
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
|
||||
@@ -3505,6 +3578,8 @@
|
||||
|
||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||
|
||||
"secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
|
||||
|
||||
"selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
@@ -3519,6 +3594,8 @@
|
||||
|
||||
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||
@@ -3581,6 +3658,8 @@
|
||||
|
||||
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
|
||||
|
||||
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
|
||||
|
||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
@@ -3589,6 +3668,8 @@
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
||||
@@ -3691,6 +3772,8 @@
|
||||
|
||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||
|
||||
"thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="],
|
||||
|
||||
"three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="],
|
||||
|
||||
"thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="],
|
||||
@@ -3989,6 +4072,8 @@
|
||||
|
||||
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||
|
||||
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="],
|
||||
|
||||
"@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||
@@ -4003,7 +4088,7 @@
|
||||
|
||||
"@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||
|
||||
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
|
||||
|
||||
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||
|
||||
@@ -4071,6 +4156,8 @@
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider/openai": ["openai@6.17.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-NHRpPEUPzAvFOAFs9+9pC6+HCw/iWsYsKCMPXH5Kw7BpMxqd8g/A07/1o7Gx2TWtCnzevVRyKMRFqyiHyAlqcA=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
@@ -4273,6 +4360,16 @@
|
||||
|
||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@2.0.56", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FGlqwWc3tAYqDHE8r8hQGQLcMiPUwgz90oU2QygUH930OWtCLapFkSu114DgVaIN/qoM1DUX+inv0Ee74Fgp5g=="],
|
||||
|
||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
@@ -4361,6 +4458,8 @@
|
||||
|
||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
|
||||
|
||||
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
|
||||
@@ -4805,6 +4904,14 @@
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
|
||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-4I0lpBnbAi7IZMURTMLysjrqdsNvXJf8802NrJnpdks=",
|
||||
"aarch64-linux": "sha256-WOGKsPlcQVSbL8TDr1JYO/2ucPTV2Hy0TXJKWv8EoVw=",
|
||||
"aarch64-darwin": "sha256-LuvjwGm1QsHoLxuvSSp4VsDIv02Z/rTONsU32arQMuw=",
|
||||
"x86_64-darwin": "sha256-AbglfgCWj/r+wHfle+e+D3b/xPcwwg4IK7j5iwn9nzw="
|
||||
"x86_64-linux": "sha256-ufEpxjmlJeft9tI+WxxO+Zbh1pdAaLOURCDBpoQqR0w=",
|
||||
"aarch64-linux": "sha256-z3K6W5oYZNUdV0rjoAZjvNQcifM5bXamLIrD+ZvJ4kA=",
|
||||
"aarch64-darwin": "sha256-+QikplmNhxGF2Nd4L1BG/xyl+24GVhDYMTtK6xCKy/s=",
|
||||
"x86_64-darwin": "sha256-hAcrCT2X02ymwgj/0BAmD2gF66ylGYzbfcqPta/LVEU="
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -7,6 +7,8 @@
|
||||
"packageManager": "bun@1.3.5",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
||||
"dev:web": "bun --cwd packages/app dev",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
"prepare": "husky",
|
||||
"random": "echo 'Random script'",
|
||||
@@ -98,7 +100,5 @@
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
|
||||
}
|
||||
"patchedDependencies": {}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,12 @@ import {
|
||||
import type { createSdk } from "./utils"
|
||||
|
||||
export async function defocus(page: Page) {
|
||||
await page.mouse.click(5, 5)
|
||||
await page
|
||||
.evaluate(() => {
|
||||
const el = document.activeElement
|
||||
if (el instanceof HTMLElement) el.blur()
|
||||
})
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
export async function openPalette(page: Page) {
|
||||
@@ -68,14 +73,50 @@ export async function toggleSidebar(page: Page) {
|
||||
|
||||
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)
|
||||
|
||||
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 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (opened) return
|
||||
|
||||
await toggleSidebar(page)
|
||||
await expect(page.locator("main")).not.toHaveClass(/xl:border-l/)
|
||||
await expect(main).not.toHaveClass(/xl:border-l/)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (closed) return
|
||||
|
||||
await toggleSidebar(page)
|
||||
await expect(page.locator("main")).toHaveClass(/xl:border-l/)
|
||||
await expect(main).toHaveClass(/xl:border-l/)
|
||||
}
|
||||
|
||||
export async function openSettings(page: Page) {
|
||||
@@ -182,13 +223,30 @@ export async function hoverSessionItem(page: Page, sessionID: string) {
|
||||
}
|
||||
|
||||
export async function openSessionMoreMenu(page: Page, sessionID: string) {
|
||||
const sessionEl = await hoverSessionItem(page, sessionID)
|
||||
await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`))
|
||||
|
||||
const menuTrigger = sessionEl.locator(dropdownMenuTriggerSelector).first()
|
||||
const scroller = page.locator(".session-scroller").first()
|
||||
await expect(scroller).toBeVisible()
|
||||
await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
|
||||
|
||||
const menu = page
|
||||
.locator(dropdownMenuContentSelector)
|
||||
.filter({ has: page.getByRole("menuitem", { name: /rename/i }) })
|
||||
.filter({ has: page.getByRole("menuitem", { name: /archive/i }) })
|
||||
.filter({ has: page.getByRole("menuitem", { name: /delete/i }) })
|
||||
.first()
|
||||
|
||||
const opened = await menu
|
||||
.isVisible()
|
||||
.then((x) => x)
|
||||
.catch(() => false)
|
||||
|
||||
if (opened) return menu
|
||||
|
||||
const menuTrigger = scroller.getByRole("button", { name: /more options/i }).first()
|
||||
await expect(menuTrigger).toBeVisible()
|
||||
await menuTrigger.click()
|
||||
|
||||
const menu = page.locator(dropdownMenuContentSelector).first()
|
||||
await expect(menu).toBeVisible()
|
||||
return menu
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test as base, expect } from "@playwright/test"
|
||||
import { seedProjects } from "./actions"
|
||||
import { test as base, expect, type Page } from "@playwright/test"
|
||||
import { cleanupTestProject, createTestProject, seedProjects } from "./actions"
|
||||
import { promptSelector } from "./selectors"
|
||||
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
||||
|
||||
@@ -8,6 +8,14 @@ export const settingsKey = "settings.v3"
|
||||
type TestFixtures = {
|
||||
sdk: ReturnType<typeof createSdk>
|
||||
gotoSession: (sessionID?: string) => Promise<void>
|
||||
withProject: <T>(
|
||||
callback: (project: {
|
||||
directory: string
|
||||
slug: string
|
||||
gotoSession: (sessionID?: string) => Promise<void>
|
||||
}) => Promise<T>,
|
||||
options?: { extra?: string[] },
|
||||
) => Promise<T>
|
||||
}
|
||||
|
||||
type WorkerFixtures = {
|
||||
@@ -33,17 +41,7 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
||||
await use(createSdk(directory))
|
||||
},
|
||||
gotoSession: async ({ page, directory }, use) => {
|
||||
await seedProjects(page, { directory })
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem(
|
||||
"opencode.global.dat:model",
|
||||
JSON.stringify({
|
||||
recent: [{ providerID: "opencode", modelID: "big-pickle" }],
|
||||
user: [],
|
||||
variant: {},
|
||||
}),
|
||||
)
|
||||
})
|
||||
await seedStorage(page, { directory })
|
||||
|
||||
const gotoSession = async (sessionID?: string) => {
|
||||
await page.goto(sessionPath(directory, sessionID))
|
||||
@@ -51,6 +49,39 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
||||
}
|
||||
await use(gotoSession)
|
||||
},
|
||||
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 gotoSession = async (sessionID?: string) => {
|
||||
await page.goto(sessionPath(directory, sessionID))
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
}
|
||||
|
||||
try {
|
||||
await gotoSession()
|
||||
return await callback({ directory, slug, gotoSession })
|
||||
} finally {
|
||||
await cleanupTestProject(directory)
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
|
||||
await seedProjects(page, input)
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem(
|
||||
"opencode.global.dat:model",
|
||||
JSON.stringify({
|
||||
recent: [{ providerID: "opencode", modelID: "big-pickle" }],
|
||||
user: [],
|
||||
variant: {},
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export { expect }
|
||||
|
||||
@@ -1,52 +1,53 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { openSidebar } from "../actions"
|
||||
|
||||
test("dialog edit project updates name and startup script", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
test("dialog edit project updates name and startup script", async ({ page, withProject }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
await openSidebar(page)
|
||||
await withProject(async () => {
|
||||
await openSidebar(page)
|
||||
|
||||
const open = async () => {
|
||||
const header = page.locator(".group\\/project").first()
|
||||
await header.hover()
|
||||
const trigger = header.getByRole("button", { name: "More options" }).first()
|
||||
await expect(trigger).toBeVisible()
|
||||
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 editItem.click({ force: true })
|
||||
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.getByRole("heading", { level: 2 })).toHaveText("Edit project")
|
||||
return dialog
|
||||
}
|
||||
|
||||
const name = `e2e project ${Date.now()}`
|
||||
const startup = `echo e2e_${Date.now()}`
|
||||
|
||||
const dialog = await open()
|
||||
|
||||
const nameInput = dialog.getByLabel("Name")
|
||||
await nameInput.fill(name)
|
||||
|
||||
const startupInput = dialog.getByLabel("Workspace startup script")
|
||||
await startupInput.fill(startup)
|
||||
|
||||
await dialog.getByRole("button", { name: "Save" }).click()
|
||||
await expect(dialog).toHaveCount(0)
|
||||
|
||||
const open = async () => {
|
||||
const header = page.locator(".group\\/project").first()
|
||||
await header.hover()
|
||||
const trigger = header.getByRole("button", { name: "More options" }).first()
|
||||
await expect(trigger).toBeVisible()
|
||||
await trigger.click({ force: true })
|
||||
await expect(header).toContainText(name)
|
||||
|
||||
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 editItem.click({ force: true })
|
||||
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.getByRole("heading", { level: 2 })).toHaveText("Edit project")
|
||||
return dialog
|
||||
}
|
||||
|
||||
const name = `e2e project ${Date.now()}`
|
||||
const startup = `echo e2e_${Date.now()}`
|
||||
|
||||
const dialog = await open()
|
||||
|
||||
const nameInput = dialog.getByLabel("Name")
|
||||
await nameInput.fill(name)
|
||||
|
||||
const startupInput = dialog.getByLabel("Workspace startup script")
|
||||
await startupInput.fill(startup)
|
||||
|
||||
await dialog.getByRole("button", { name: "Save" }).click()
|
||||
await expect(dialog).toHaveCount(0)
|
||||
|
||||
const header = page.locator(".group\\/project").first()
|
||||
await expect(header).toContainText(name)
|
||||
|
||||
const reopened = await open()
|
||||
await expect(reopened.getByLabel("Name")).toHaveValue(name)
|
||||
await expect(reopened.getByLabel("Workspace startup script")).toHaveValue(startup)
|
||||
await reopened.getByRole("button", { name: "Cancel" }).click()
|
||||
await expect(reopened).toHaveCount(0)
|
||||
const reopened = await open()
|
||||
await expect(reopened.getByLabel("Name")).toHaveValue(name)
|
||||
await expect(reopened.getByLabel("Workspace startup script")).toHaveValue(startup)
|
||||
await reopened.getByRole("button", { name: "Cancel" }).click()
|
||||
await expect(reopened).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,69 +1,73 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { createTestProject, seedProjects, cleanupTestProject, openSidebar, clickMenuItem } from "../actions"
|
||||
import { createTestProject, cleanupTestProject, openSidebar, clickMenuItem } from "../actions"
|
||||
import { projectCloseHoverSelector, projectCloseMenuSelector, projectSwitchSelector } from "../selectors"
|
||||
import { dirSlug } from "../utils"
|
||||
|
||||
test("can close a project via hover card close button", async ({ page, directory, gotoSession }) => {
|
||||
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)
|
||||
await seedProjects(page, { directory, extra: [other] })
|
||||
|
||||
try {
|
||||
await gotoSession()
|
||||
await withProject(
|
||||
async () => {
|
||||
await openSidebar(page)
|
||||
|
||||
await openSidebar(page)
|
||||
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
||||
await expect(otherButton).toBeVisible()
|
||||
await otherButton.hover()
|
||||
|
||||
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()
|
||||
|
||||
const close = page.locator(projectCloseHoverSelector(otherSlug)).first()
|
||||
await expect(close).toBeVisible()
|
||||
await close.click()
|
||||
|
||||
await expect(otherButton).toHaveCount(0)
|
||||
await expect(otherButton).toHaveCount(0)
|
||||
},
|
||||
{ extra: [other] },
|
||||
)
|
||||
} finally {
|
||||
await cleanupTestProject(other)
|
||||
}
|
||||
})
|
||||
|
||||
test("can close a project via project header more options menu", async ({ page, directory, gotoSession }) => {
|
||||
test("can close a project via project header more options menu", async ({ page, withProject }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
const other = await createTestProject()
|
||||
const otherName = other.split("/").pop() ?? other
|
||||
const otherSlug = dirSlug(other)
|
||||
await seedProjects(page, { directory, extra: [other] })
|
||||
|
||||
try {
|
||||
await gotoSession()
|
||||
await withProject(
|
||||
async () => {
|
||||
await openSidebar(page)
|
||||
|
||||
await openSidebar(page)
|
||||
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
||||
await expect(otherButton).toBeVisible()
|
||||
await otherButton.click()
|
||||
|
||||
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
||||
await expect(otherButton).toBeVisible()
|
||||
await otherButton.click()
|
||||
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
||||
const header = page
|
||||
.locator(".group\\/project")
|
||||
.filter({ has: page.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`) })
|
||||
.first()
|
||||
await expect(header).toContainText(otherName)
|
||||
|
||||
const header = page
|
||||
.locator(".group\\/project")
|
||||
.filter({ has: page.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`) })
|
||||
.first()
|
||||
await expect(header).toContainText(otherName)
|
||||
const trigger = header.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`).first()
|
||||
await expect(trigger).toHaveCount(1)
|
||||
await trigger.focus()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
const trigger = header.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`).first()
|
||||
await expect(trigger).toHaveCount(1)
|
||||
await trigger.focus()
|
||||
await page.keyboard.press("Enter")
|
||||
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
|
||||
await expect(menu).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
|
||||
await expect(menu).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
await clickMenuItem(menu, /^Close$/i, { force: true })
|
||||
await expect(otherButton).toHaveCount(0)
|
||||
await clickMenuItem(menu, /^Close$/i, { force: true })
|
||||
await expect(otherButton).toHaveCount(0)
|
||||
},
|
||||
{ extra: [other] },
|
||||
)
|
||||
} finally {
|
||||
await cleanupTestProject(other)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { defocus, createTestProject, seedProjects, cleanupTestProject } from "../actions"
|
||||
import { defocus, createTestProject, cleanupTestProject } from "../actions"
|
||||
import { projectSwitchSelector } from "../selectors"
|
||||
import { dirSlug } from "../utils"
|
||||
|
||||
test("can switch between projects from sidebar", async ({ page, directory, gotoSession }) => {
|
||||
test("can switch between projects from sidebar", async ({ page, withProject }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
const other = await createTestProject()
|
||||
const otherSlug = dirSlug(other)
|
||||
|
||||
await seedProjects(page, { directory, extra: [other] })
|
||||
|
||||
try {
|
||||
await gotoSession()
|
||||
await withProject(
|
||||
async ({ directory }) => {
|
||||
await defocus(page)
|
||||
|
||||
await defocus(page)
|
||||
const currentSlug = dirSlug(directory)
|
||||
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
||||
await expect(otherButton).toBeVisible()
|
||||
await otherButton.click()
|
||||
|
||||
const currentSlug = dirSlug(directory)
|
||||
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
||||
await expect(otherButton).toBeVisible()
|
||||
await otherButton.click()
|
||||
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
||||
const currentButton = page.locator(projectSwitchSelector(currentSlug)).first()
|
||||
await expect(currentButton).toBeVisible()
|
||||
await currentButton.click()
|
||||
|
||||
const currentButton = page.locator(projectSwitchSelector(currentSlug)).first()
|
||||
await expect(currentButton).toBeVisible()
|
||||
await currentButton.click()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`))
|
||||
await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`))
|
||||
},
|
||||
{ extra: [other] },
|
||||
)
|
||||
} finally {
|
||||
await cleanupTestProject(other)
|
||||
}
|
||||
|
||||
@@ -10,33 +10,20 @@ import {
|
||||
cleanupTestProject,
|
||||
clickMenuItem,
|
||||
confirmDialog,
|
||||
createTestProject,
|
||||
openSidebar,
|
||||
openWorkspaceMenu,
|
||||
seedProjects,
|
||||
setWorkspacesEnabled,
|
||||
} from "../actions"
|
||||
import { inlineInputSelector, projectSwitchSelector, workspaceItemSelector } from "../selectors"
|
||||
import { dirSlug } from "../utils"
|
||||
import { inlineInputSelector, workspaceItemSelector } from "../selectors"
|
||||
|
||||
function slugFromUrl(url: string) {
|
||||
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
||||
}
|
||||
|
||||
async function setupWorkspaceTest(page: Page, directory: string, gotoSession: () => Promise<void>) {
|
||||
const project = await createTestProject()
|
||||
const rootSlug = dirSlug(project)
|
||||
await seedProjects(page, { directory, extra: [project] })
|
||||
|
||||
await gotoSession()
|
||||
async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
||||
const rootSlug = project.slug
|
||||
await openSidebar(page)
|
||||
|
||||
const target = page.locator(projectSwitchSelector(rootSlug)).first()
|
||||
await expect(target).toBeVisible()
|
||||
await target.click()
|
||||
await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
|
||||
|
||||
await openSidebar(page)
|
||||
await setWorkspacesEnabled(page, rootSlug, true)
|
||||
|
||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||
@@ -70,25 +57,13 @@ async function setupWorkspaceTest(page: Page, directory: string, gotoSession: ()
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
return { project, rootSlug, slug, directory: dir }
|
||||
return { rootSlug, slug, directory: dir }
|
||||
}
|
||||
|
||||
test("can enable and disable workspaces from project menu", async ({ page, directory, gotoSession }) => {
|
||||
test("can enable and disable workspaces from project menu", async ({ page, withProject }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
const project = await createTestProject()
|
||||
const slug = dirSlug(project)
|
||||
await seedProjects(page, { directory, extra: [project] })
|
||||
|
||||
try {
|
||||
await gotoSession()
|
||||
await openSidebar(page)
|
||||
|
||||
const target = page.locator(projectSwitchSelector(slug)).first()
|
||||
await expect(target).toBeVisible()
|
||||
await target.click()
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session`))
|
||||
|
||||
await withProject(async ({ slug }) => {
|
||||
await openSidebar(page)
|
||||
|
||||
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
|
||||
@@ -101,27 +76,13 @@ test("can enable and disable workspaces from project menu", async ({ page, direc
|
||||
await setWorkspacesEnabled(page, slug, false)
|
||||
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
|
||||
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
|
||||
} finally {
|
||||
await cleanupTestProject(project)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("can create a workspace", async ({ page, directory, gotoSession }) => {
|
||||
test("can create a workspace", async ({ page, withProject }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
const project = await createTestProject()
|
||||
const slug = dirSlug(project)
|
||||
await seedProjects(page, { directory, extra: [project] })
|
||||
|
||||
try {
|
||||
await gotoSession()
|
||||
await openSidebar(page)
|
||||
|
||||
const target = page.locator(projectSwitchSelector(slug)).first()
|
||||
await expect(target).toBeVisible()
|
||||
await target.click()
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session`))
|
||||
|
||||
await withProject(async ({ slug }) => {
|
||||
await openSidebar(page)
|
||||
await setWorkspacesEnabled(page, slug, true)
|
||||
|
||||
@@ -162,17 +123,15 @@ test("can create a workspace", async ({ page, directory, gotoSession }) => {
|
||||
await expect(page.locator(workspaceItemSelector(workspaceSlug)).first()).toBeVisible()
|
||||
|
||||
await cleanupTestProject(workspaceDir)
|
||||
} finally {
|
||||
await cleanupTestProject(project)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("can rename a workspace", async ({ page, directory, gotoSession }) => {
|
||||
test("can rename a workspace", async ({ page, withProject }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
const { project, slug } = await setupWorkspaceTest(page, directory, gotoSession)
|
||||
await withProject(async (project) => {
|
||||
const { slug } = await setupWorkspaceTest(page, project)
|
||||
|
||||
try {
|
||||
const rename = `e2e workspace ${Date.now()}`
|
||||
const menu = await openWorkspaceMenu(page, slug)
|
||||
await clickMenuItem(menu, /^Rename$/i, { force: true })
|
||||
@@ -186,17 +145,15 @@ test("can rename a workspace", async ({ page, directory, gotoSession }) => {
|
||||
await input.fill(rename)
|
||||
await input.press("Enter")
|
||||
await expect(item).toContainText(rename)
|
||||
} finally {
|
||||
await cleanupTestProject(project)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("can reset a workspace", async ({ page, directory, sdk, gotoSession }) => {
|
||||
test("can reset a workspace", async ({ page, sdk, withProject }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
const { project, slug, directory: createdDir } = await setupWorkspaceTest(page, directory, gotoSession)
|
||||
await withProject(async (project) => {
|
||||
const { slug, directory: createdDir } = await setupWorkspaceTest(page, project)
|
||||
|
||||
try {
|
||||
const readme = path.join(createdDir, "README.md")
|
||||
const extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`)
|
||||
const original = await fs.readFile(readme, "utf8")
|
||||
@@ -250,17 +207,15 @@ test("can reset a workspace", async ({ page, directory, sdk, gotoSession }) => {
|
||||
.catch(() => false)
|
||||
})
|
||||
.toBe(false)
|
||||
} finally {
|
||||
await cleanupTestProject(project)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("can delete a workspace", async ({ page, directory, gotoSession }) => {
|
||||
test("can delete a workspace", async ({ page, withProject }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
const { project, rootSlug, slug } = await setupWorkspaceTest(page, directory, gotoSession)
|
||||
await withProject(async (project) => {
|
||||
const { rootSlug, slug } = await setupWorkspaceTest(page, project)
|
||||
|
||||
try {
|
||||
const menu = await openWorkspaceMenu(page, slug)
|
||||
await clickMenuItem(menu, /^Delete$/i, { force: true })
|
||||
await confirmDialog(page, /^Delete workspace$/i)
|
||||
@@ -268,124 +223,111 @@ test("can delete a workspace", async ({ page, directory, gotoSession }) => {
|
||||
await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
|
||||
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
|
||||
await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
|
||||
} finally {
|
||||
await cleanupTestProject(project)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("can reorder workspaces by drag and drop", async ({ page, directory, gotoSession }) => {
|
||||
test("can reorder workspaces by drag and drop", async ({ page, withProject }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
await withProject(async ({ slug: rootSlug }) => {
|
||||
const workspaces = [] as { directory: string; slug: string }[]
|
||||
|
||||
const project = await createTestProject()
|
||||
const rootSlug = dirSlug(project)
|
||||
await seedProjects(page, { directory, extra: [project] })
|
||||
const listSlugs = async () => {
|
||||
const nodes = page.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]')
|
||||
const slugs = await nodes.evaluateAll((els) => {
|
||||
return els.map((el) => el.getAttribute("data-workspace") ?? "").filter((x) => x.length > 0)
|
||||
})
|
||||
return slugs
|
||||
}
|
||||
|
||||
const workspaces = [] as { directory: string; slug: string }[]
|
||||
|
||||
const listSlugs = async () => {
|
||||
const nodes = page.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]')
|
||||
const slugs = await nodes.evaluateAll((els) => {
|
||||
return els.map((el) => el.getAttribute("data-workspace") ?? "").filter((x) => x.length > 0)
|
||||
})
|
||||
return slugs
|
||||
}
|
||||
|
||||
const waitReady = async (slug: string) => {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const item = page.locator(workspaceItemSelector(slug)).first()
|
||||
try {
|
||||
await item.hover({ timeout: 500 })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
}
|
||||
|
||||
const drag = async (from: string, to: string) => {
|
||||
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")
|
||||
|
||||
await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2, { steps: 12 })
|
||||
await page.mouse.up()
|
||||
}
|
||||
|
||||
try {
|
||||
await gotoSession()
|
||||
await openSidebar(page)
|
||||
|
||||
const target = page.locator(projectSwitchSelector(rootSlug)).first()
|
||||
await expect(target).toBeVisible()
|
||||
await target.click()
|
||||
await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
|
||||
|
||||
await openSidebar(page)
|
||||
await setWorkspacesEnabled(page, rootSlug, true)
|
||||
|
||||
for (const _ of [0, 1]) {
|
||||
const prev = slugFromUrl(page.url())
|
||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||
const waitReady = async (slug: string) => {
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
const slug = slugFromUrl(page.url())
|
||||
return slug.length > 0 && slug !== rootSlug && slug !== prev
|
||||
async () => {
|
||||
const item = page.locator(workspaceItemSelector(slug)).first()
|
||||
try {
|
||||
await item.hover({ timeout: 500 })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
{ timeout: 45_000 },
|
||||
{ timeout: 60_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
}
|
||||
|
||||
const slug = slugFromUrl(page.url())
|
||||
const dir = base64Decode(slug)
|
||||
workspaces.push({ slug, directory: dir })
|
||||
const drag = async (from: string, to: string) => {
|
||||
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")
|
||||
|
||||
await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2, { steps: 12 })
|
||||
await page.mouse.up()
|
||||
}
|
||||
|
||||
try {
|
||||
await openSidebar(page)
|
||||
|
||||
await setWorkspacesEnabled(page, rootSlug, true)
|
||||
|
||||
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 dir = base64Decode(slug)
|
||||
workspaces.push({ slug, directory: dir })
|
||||
|
||||
await openSidebar(page)
|
||||
}
|
||||
|
||||
if (workspaces.length !== 2) throw new Error("Expected two created workspaces")
|
||||
|
||||
const a = workspaces[0].slug
|
||||
const b = workspaces[1].slug
|
||||
|
||||
await waitReady(a)
|
||||
await waitReady(b)
|
||||
|
||||
const list = async () => {
|
||||
const slugs = await listSlugs()
|
||||
return slugs.filter((s) => s !== rootSlug && (s === a || s === b)).slice(0, 2)
|
||||
}
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const slugs = await list()
|
||||
return slugs.length === 2
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
const before = await list()
|
||||
const from = before[1]
|
||||
const to = before[0]
|
||||
if (!from || !to) throw new Error("Failed to resolve initial workspace order")
|
||||
|
||||
await drag(from, to)
|
||||
|
||||
await expect.poll(async () => await list()).toEqual([from, to])
|
||||
} finally {
|
||||
await Promise.all(workspaces.map((w) => cleanupTestProject(w.directory)))
|
||||
}
|
||||
|
||||
if (workspaces.length !== 2) throw new Error("Expected two created workspaces")
|
||||
|
||||
const a = workspaces[0].slug
|
||||
const b = workspaces[1].slug
|
||||
|
||||
await waitReady(a)
|
||||
await waitReady(b)
|
||||
|
||||
const list = async () => {
|
||||
const slugs = await listSlugs()
|
||||
return slugs.filter((s) => s !== rootSlug && (s === a || s === b)).slice(0, 2)
|
||||
}
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const slugs = await list()
|
||||
return slugs.length === 2
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
const before = await list()
|
||||
const from = before[1]
|
||||
const to = before[0]
|
||||
if (!from || !to) throw new Error("Failed to resolve initial workspace order")
|
||||
|
||||
await drag(from, to)
|
||||
|
||||
await expect.poll(async () => await list()).toEqual([from, to])
|
||||
} finally {
|
||||
await Promise.all(workspaces.map((w) => cleanupTestProject(w.directory)))
|
||||
await cleanupTestProject(project)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,57 +11,98 @@ import { sessionItemSelector, inlineInputSelector } from "../selectors"
|
||||
|
||||
const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1"
|
||||
|
||||
test("sidebar session can be renamed", async ({ page, sdk, gotoSession }) => {
|
||||
type Sdk = Parameters<typeof withSession>[0]
|
||||
|
||||
async function seedMessage(sdk: Sdk, sessionID: string) {
|
||||
await sdk.session.promptAsync({
|
||||
sessionID,
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "e2e 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("session can be renamed via header menu", async ({ page, sdk, gotoSession }) => {
|
||||
const stamp = Date.now()
|
||||
const originalTitle = `e2e rename test ${stamp}`
|
||||
const newTitle = `e2e renamed ${stamp}`
|
||||
|
||||
await withSession(sdk, originalTitle, async (session) => {
|
||||
await seedMessage(sdk, session.id)
|
||||
await gotoSession(session.id)
|
||||
await openSidebar(page)
|
||||
|
||||
const menu = await openSessionMoreMenu(page, session.id)
|
||||
await clickMenuItem(menu, /rename/i)
|
||||
|
||||
const input = page.locator(sessionItemSelector(session.id)).locator(inlineInputSelector).first()
|
||||
const input = page.locator(".session-scroller").locator(inlineInputSelector).first()
|
||||
await expect(input).toBeVisible()
|
||||
await input.fill(newTitle)
|
||||
await input.press("Enter")
|
||||
|
||||
await expect(page.locator(sessionItemSelector(session.id)).locator("a").first()).toContainText(newTitle)
|
||||
await expect(page.getByRole("heading", { level: 1 }).first()).toContainText(newTitle)
|
||||
})
|
||||
})
|
||||
|
||||
test("sidebar session can be archived", async ({ page, sdk, gotoSession }) => {
|
||||
test("session can be archived via header menu", async ({ page, sdk, gotoSession }) => {
|
||||
const stamp = Date.now()
|
||||
const title = `e2e archive test ${stamp}`
|
||||
|
||||
await withSession(sdk, title, async (session) => {
|
||||
await seedMessage(sdk, session.id)
|
||||
await gotoSession(session.id)
|
||||
await openSidebar(page)
|
||||
|
||||
const sessionEl = page.locator(sessionItemSelector(session.id))
|
||||
const menu = await openSessionMoreMenu(page, session.id)
|
||||
await clickMenuItem(menu, /archive/i)
|
||||
|
||||
await expect(sessionEl).not.toBeVisible()
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
|
||||
return data?.time?.archived
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.not.toBeUndefined()
|
||||
|
||||
await openSidebar(page)
|
||||
await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
test("sidebar session can be deleted", async ({ page, sdk, gotoSession }) => {
|
||||
test("session can be deleted via header menu", async ({ page, sdk, gotoSession }) => {
|
||||
const stamp = Date.now()
|
||||
const title = `e2e delete test ${stamp}`
|
||||
|
||||
await withSession(sdk, title, async (session) => {
|
||||
await seedMessage(sdk, session.id)
|
||||
await gotoSession(session.id)
|
||||
await openSidebar(page)
|
||||
|
||||
const sessionEl = page.locator(sessionItemSelector(session.id))
|
||||
const menu = await openSessionMoreMenu(page, session.id)
|
||||
await clickMenuItem(menu, /delete/i)
|
||||
await confirmDialog(page, /delete/i)
|
||||
|
||||
await expect(sessionEl).not.toBeVisible()
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const data = await sdk.session
|
||||
.get({ sessionID: session.id })
|
||||
.then((r) => r.data)
|
||||
.catch(() => undefined)
|
||||
return data?.id
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBeUndefined()
|
||||
|
||||
await openSidebar(page)
|
||||
await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -72,6 +113,7 @@ test("session can be shared and unshared via header button", async ({ page, sdk,
|
||||
const title = `e2e share test ${stamp}`
|
||||
|
||||
await withSession(sdk, title, async (session) => {
|
||||
await seedMessage(sdk, session.id)
|
||||
await gotoSession(session.id)
|
||||
|
||||
const { rightSection, popoverBody } = await openSharePopover(page)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { openStatusPopover, defocus } from "../actions"
|
||||
import { openStatusPopover } from "../actions"
|
||||
|
||||
test("status popover opens and shows tabs", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
@@ -88,7 +88,7 @@ test("status popover closes when clicking outside", async ({ page, gotoSession }
|
||||
const { popoverBody } = await openStatusPopover(page)
|
||||
await expect(popoverBody).toBeVisible()
|
||||
|
||||
await defocus(page)
|
||||
await page.getByRole("main").click({ position: { x: 5, y: 5 } })
|
||||
|
||||
await expect(popoverBody).toHaveCount(0)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -54,7 +54,7 @@
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"ghostty-web": "0.3.0",
|
||||
"ghostty-web": "0.4.0",
|
||||
"luxon": "catalog:",
|
||||
"marked": "catalog:",
|
||||
"marked-shiki": "catalog:",
|
||||
|
||||
@@ -6,7 +6,6 @@ const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost"
|
||||
const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||
const command = `bun run dev -- --host 0.0.0.0 --port ${port}`
|
||||
const reuse = !process.env.CI
|
||||
const win = process.platform === "win32"
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./e2e",
|
||||
@@ -15,8 +14,7 @@ export default defineConfig({
|
||||
expect: {
|
||||
timeout: 10_000,
|
||||
},
|
||||
fullyParallel: !win,
|
||||
workers: win ? 1 : undefined,
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]],
|
||||
|
||||
@@ -241,19 +241,19 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||
let rowSeparator = ""
|
||||
|
||||
if (this._nullCellCount > 0) {
|
||||
const nextLine = isLastRow ? undefined : this._buffer.getLine(row + 1)
|
||||
const wrapped = !!nextLine?.isWrapped
|
||||
|
||||
if (this._nullCellCount > 0 && wrapped) {
|
||||
this._currentRow += " ".repeat(this._nullCellCount)
|
||||
this._nullCellCount = 0
|
||||
}
|
||||
|
||||
if (!isLastRow) {
|
||||
const nextLine = this._buffer.getLine(row + 1)
|
||||
this._nullCellCount = 0
|
||||
|
||||
if (!nextLine?.isWrapped) {
|
||||
rowSeparator = "\r\n"
|
||||
this._lastCursorRow = row + 1
|
||||
this._lastCursorCol = 0
|
||||
}
|
||||
if (!isLastRow && !wrapped) {
|
||||
rowSeparator = "\r\n"
|
||||
this._lastCursorRow = row + 1
|
||||
this._lastCursorCol = 0
|
||||
}
|
||||
|
||||
this._allRows[this._rowIndex] = this._currentRow
|
||||
@@ -389,7 +389,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
const sgrSeq = this._diffStyle(cell, this._cursorStyle)
|
||||
|
||||
const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0
|
||||
const styleChanged = sgrSeq.length > 0
|
||||
|
||||
if (styleChanged) {
|
||||
if (this._nullCellCount > 0) {
|
||||
@@ -442,12 +442,24 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (!excludeFinalCursorPosition) {
|
||||
const absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY
|
||||
const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER)
|
||||
const cursorCol = this._buffer.cursorX + 1
|
||||
content += `\u001b[${cursorRow};${cursorCol}H`
|
||||
}
|
||||
if (excludeFinalCursorPosition) return content
|
||||
|
||||
const absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY
|
||||
const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER)
|
||||
const cursorCol = this._buffer.cursorX + 1
|
||||
content += `\u001b[${cursorRow};${cursorCol}H`
|
||||
|
||||
const line = this._buffer.getLine(absoluteCursorRow)
|
||||
const cell = line?.getCell(this._buffer.cursorX)
|
||||
const style = (() => {
|
||||
if (!cell) return this._buffer.getNullCell()
|
||||
if (cell.getWidth() !== 0) return cell
|
||||
if (this._buffer.cursorX > 0) return line?.getCell(this._buffer.cursorX - 1) ?? cell
|
||||
return cell
|
||||
})()
|
||||
|
||||
const sgrSeq = this._diffStyle(style, this._cursorStyle)
|
||||
if (sgrSeq.length) content += `\u001b[${sgrSeq.join(";")}m`
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -158,22 +158,22 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
</Show>
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 size-16 bg-black/60 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||
classList={{
|
||||
"opacity-100": store.iconHover && !store.iconUrl,
|
||||
"opacity-0": !(store.iconHover && !store.iconUrl),
|
||||
}}
|
||||
>
|
||||
<Icon name="cloud-upload" size="large" class="text-icon-invert-base" />
|
||||
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 size-16 bg-black/60 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||
classList={{
|
||||
"opacity-100": store.iconHover && !!store.iconUrl,
|
||||
"opacity-0": !(store.iconHover && !!store.iconUrl),
|
||||
}}
|
||||
>
|
||||
<Icon name="trash" size="large" class="text-icon-invert-base" />
|
||||
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||
</div>
|
||||
</div>
|
||||
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
|
||||
|
||||
@@ -4,10 +4,11 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { createMemo } from "solid-js"
|
||||
import { createMemo, createResource, createSignal } from "solid-js"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import type { ListRef } from "@opencode-ai/ui/list"
|
||||
|
||||
interface DialogSelectDirectoryProps {
|
||||
title?: string
|
||||
@@ -15,18 +16,47 @@ interface DialogSelectDirectoryProps {
|
||||
onSelect: (result: string | string[] | null) => void
|
||||
}
|
||||
|
||||
type Row = {
|
||||
absolute: string
|
||||
search: string
|
||||
}
|
||||
|
||||
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
const sync = useGlobalSync()
|
||||
const sdk = useGlobalSDK()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
|
||||
const home = createMemo(() => sync.data.path.home)
|
||||
const [filter, setFilter] = createSignal("")
|
||||
|
||||
const start = createMemo(() => sync.data.path.home || sync.data.path.directory)
|
||||
let list: ListRef | undefined
|
||||
|
||||
const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
|
||||
|
||||
const [fallbackPath] = createResource(
|
||||
() => (missingBase() ? true : undefined),
|
||||
async () => {
|
||||
return sdk.client.path
|
||||
.get()
|
||||
.then((x) => x.data)
|
||||
.catch(() => undefined)
|
||||
},
|
||||
{ initialValue: undefined },
|
||||
)
|
||||
|
||||
const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "")
|
||||
|
||||
const start = createMemo(
|
||||
() => sync.data.path.home || sync.data.path.directory || fallbackPath()?.home || fallbackPath()?.directory,
|
||||
)
|
||||
|
||||
const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>()
|
||||
|
||||
const clean = (value: string) => {
|
||||
const first = (value ?? "").split(/\r?\n/)[0] ?? ""
|
||||
return first.replace(/[\u0000-\u001F\u007F]/g, "").trim()
|
||||
}
|
||||
|
||||
function normalize(input: string) {
|
||||
const v = input.replaceAll("\\", "/")
|
||||
if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/")
|
||||
@@ -64,24 +94,67 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
return ""
|
||||
}
|
||||
|
||||
function display(path: string) {
|
||||
function parentOf(input: string) {
|
||||
const v = trimTrailing(input)
|
||||
if (v === "/") return v
|
||||
if (v === "//") return v
|
||||
if (/^[A-Za-z]:\/$/.test(v)) return v
|
||||
|
||||
const i = v.lastIndexOf("/")
|
||||
if (i <= 0) return "/"
|
||||
if (i === 2 && /^[A-Za-z]:/.test(v)) return v.slice(0, 3)
|
||||
return v.slice(0, i)
|
||||
}
|
||||
|
||||
function modeOf(input: string) {
|
||||
const raw = normalizeDriveRoot(input.trim())
|
||||
if (!raw) return "relative" as const
|
||||
if (raw.startsWith("~")) return "tilde" as const
|
||||
if (rootOf(raw)) return "absolute" as const
|
||||
return "relative" as const
|
||||
}
|
||||
|
||||
function display(path: string, input: string) {
|
||||
const full = trimTrailing(path)
|
||||
if (modeOf(input) === "absolute") return full
|
||||
|
||||
return tildeOf(full) || full
|
||||
}
|
||||
|
||||
function tildeOf(absolute: string) {
|
||||
const full = trimTrailing(absolute)
|
||||
const h = home()
|
||||
if (!h) return full
|
||||
if (!h) return ""
|
||||
|
||||
const hn = trimTrailing(h)
|
||||
const lc = full.toLowerCase()
|
||||
const hc = hn.toLowerCase()
|
||||
if (lc === hc) return "~"
|
||||
if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length)
|
||||
return full
|
||||
return ""
|
||||
}
|
||||
|
||||
function scoped(filter: string) {
|
||||
function row(absolute: string): Row {
|
||||
const full = trimTrailing(absolute)
|
||||
const tilde = tildeOf(full)
|
||||
|
||||
const withSlash = (value: string) => {
|
||||
if (!value) return ""
|
||||
if (value.endsWith("/")) return value
|
||||
return value + "/"
|
||||
}
|
||||
|
||||
const search = Array.from(
|
||||
new Set([full, withSlash(full), tilde, withSlash(tilde), getFilename(full)].filter(Boolean)),
|
||||
).join("\n")
|
||||
return { absolute: full, search }
|
||||
}
|
||||
|
||||
function scoped(value: string) {
|
||||
const base = start()
|
||||
if (!base) return
|
||||
|
||||
const raw = normalizeDriveRoot(filter.trim())
|
||||
const raw = normalizeDriveRoot(value)
|
||||
if (!raw) return { directory: trimTrailing(base), path: "" }
|
||||
|
||||
const h = home()
|
||||
@@ -122,21 +195,25 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
}
|
||||
|
||||
const directories = async (filter: string) => {
|
||||
const input = scoped(filter)
|
||||
if (!input) return [] as string[]
|
||||
const value = clean(filter)
|
||||
const scopedInput = scoped(value)
|
||||
if (!scopedInput) return [] as string[]
|
||||
|
||||
const raw = normalizeDriveRoot(filter.trim())
|
||||
const raw = normalizeDriveRoot(value)
|
||||
const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/")
|
||||
|
||||
const query = normalizeDriveRoot(input.path)
|
||||
const query = normalizeDriveRoot(scopedInput.path)
|
||||
|
||||
if (!isPath) {
|
||||
const results = await sdk.client.find
|
||||
.files({ directory: input.directory, query, type: "directory", limit: 50 })
|
||||
const find = () =>
|
||||
sdk.client.find
|
||||
.files({ directory: scopedInput.directory, query, type: "directory", limit: 50 })
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => [])
|
||||
|
||||
return results.map((rel) => join(input.directory, rel)).slice(0, 50)
|
||||
if (!isPath) {
|
||||
const results = await find()
|
||||
|
||||
return results.map((rel) => join(scopedInput.directory, rel)).slice(0, 50)
|
||||
}
|
||||
|
||||
const segments = query.replace(/^\/+/, "").split("/")
|
||||
@@ -145,17 +222,10 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
|
||||
const cap = 12
|
||||
const branch = 4
|
||||
let paths = [input.directory]
|
||||
let paths = [scopedInput.directory]
|
||||
for (const part of head) {
|
||||
if (part === "..") {
|
||||
paths = paths.map((p) => {
|
||||
const v = trimTrailing(p)
|
||||
if (v === "/") return v
|
||||
if (/^[A-Za-z]:\/$/.test(v)) return v
|
||||
const i = v.lastIndexOf("/")
|
||||
if (i <= 0) return "/"
|
||||
return v.slice(0, i)
|
||||
})
|
||||
paths = paths.map(parentOf)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -165,7 +235,27 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
}
|
||||
|
||||
const out = (await Promise.all(paths.map((p) => match(p, tail, 50)))).flat()
|
||||
return Array.from(new Set(out)).slice(0, 50)
|
||||
const deduped = Array.from(new Set(out))
|
||||
const base = raw.startsWith("~") ? trimTrailing(scopedInput.directory) : ""
|
||||
const expand = !raw.endsWith("/")
|
||||
if (!expand || !tail) {
|
||||
const items = base ? Array.from(new Set([base, ...deduped])) : deduped
|
||||
return items.slice(0, 50)
|
||||
}
|
||||
|
||||
const needle = tail.toLowerCase()
|
||||
const exact = deduped.filter((p) => getFilename(p).toLowerCase() === needle)
|
||||
const target = exact[0]
|
||||
if (!target) return deduped.slice(0, 50)
|
||||
|
||||
const children = await match(target, "", 30)
|
||||
const items = Array.from(new Set([...deduped, ...children]))
|
||||
return (base ? Array.from(new Set([base, ...items])) : items).slice(0, 50)
|
||||
}
|
||||
|
||||
const items = async (value: string) => {
|
||||
const results = await directories(value)
|
||||
return results.map(row)
|
||||
}
|
||||
|
||||
function resolve(absolute: string) {
|
||||
@@ -179,24 +269,52 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.directory.empty")}
|
||||
loadingMessage={language.t("common.loading")}
|
||||
items={directories}
|
||||
key={(x) => x}
|
||||
items={items}
|
||||
key={(x) => x.absolute}
|
||||
filterKeys={["search"]}
|
||||
ref={(r) => (list = r)}
|
||||
onFilter={(value) => setFilter(clean(value))}
|
||||
onKeyEvent={(e, item) => {
|
||||
if (e.key !== "Tab") return
|
||||
if (e.shiftKey) return
|
||||
if (!item) return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const value = display(item.absolute, filter())
|
||||
list?.setFilter(value.endsWith("/") ? value : value + "/")
|
||||
}}
|
||||
onSelect={(path) => {
|
||||
if (!path) return
|
||||
resolve(path)
|
||||
resolve(path.absolute)
|
||||
}}
|
||||
>
|
||||
{(absolute) => {
|
||||
const path = display(absolute)
|
||||
{(item) => {
|
||||
const path = display(item.absolute, filter())
|
||||
if (path === "~") {
|
||||
return (
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: item.absolute, type: "directory" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular min-w-0">
|
||||
<span class="text-text-strong whitespace-nowrap">~</span>
|
||||
<span class="text-text-weak whitespace-nowrap">/</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: absolute, type: "directory" }} class="shrink-0 size-4" />
|
||||
<FileIcon node={{ path: item.absolute, type: "directory" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular min-w-0">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(path)}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(path)}</span>
|
||||
<span class="text-text-weak whitespace-nowrap">/</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createMemo, createSignal, onCleanup, Show } from "solid-js"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useFile } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
|
||||
type EntryType = "command" | "file"
|
||||
type EntryType = "command" | "file" | "session"
|
||||
|
||||
type Entry = {
|
||||
id: string
|
||||
@@ -22,6 +27,9 @@ type Entry = {
|
||||
category: string
|
||||
option?: CommandOption
|
||||
path?: string
|
||||
directory?: string
|
||||
sessionID?: string
|
||||
archived?: number
|
||||
}
|
||||
|
||||
type DialogSelectFileMode = "all" | "files"
|
||||
@@ -33,9 +41,13 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
const file = useFile()
|
||||
const dialog = useDialog()
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const filesOnly = () => props.mode === "files"
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
const [grouped, setGrouped] = createSignal(false)
|
||||
const common = [
|
||||
@@ -73,6 +85,52 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
path,
|
||||
})
|
||||
|
||||
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||
const project = createMemo(() => {
|
||||
const directory = projectDirectory()
|
||||
if (!directory) return
|
||||
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
|
||||
})
|
||||
const workspaces = createMemo(() => {
|
||||
const directory = projectDirectory()
|
||||
const current = project()
|
||||
if (!current) return directory ? [directory] : []
|
||||
|
||||
const dirs = [current.worktree, ...(current.sandboxes ?? [])]
|
||||
if (directory && !dirs.includes(directory)) return [...dirs, directory]
|
||||
return dirs
|
||||
})
|
||||
const homedir = createMemo(() => globalSync.data.path.home)
|
||||
const label = (directory: string) => {
|
||||
const current = project()
|
||||
const kind =
|
||||
current && directory === current.worktree
|
||||
? language.t("workspace.type.local")
|
||||
: language.t("workspace.type.sandbox")
|
||||
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||
const home = homedir()
|
||||
const path = home ? directory.replace(home, "~") : directory
|
||||
const name = store.vcs?.branch ?? getFilename(directory)
|
||||
return `${kind} : ${name || path}`
|
||||
}
|
||||
|
||||
const sessionItem = (input: {
|
||||
directory: string
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
archived?: number
|
||||
}): Entry => ({
|
||||
id: `session:${input.directory}:${input.id}`,
|
||||
type: "session",
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
category: language.t("command.category.session"),
|
||||
directory: input.directory,
|
||||
sessionID: input.id,
|
||||
archived: input.archived,
|
||||
})
|
||||
|
||||
const list = createMemo(() => allowed().map(commandItem))
|
||||
|
||||
const picks = createMemo(() => {
|
||||
@@ -122,6 +180,68 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
return out
|
||||
}
|
||||
|
||||
const sessionToken = { value: 0 }
|
||||
let sessionInflight: Promise<Entry[]> | undefined
|
||||
let sessionAll: Entry[] | undefined
|
||||
|
||||
const sessions = (text: string) => {
|
||||
const query = text.trim()
|
||||
if (!query) {
|
||||
sessionToken.value += 1
|
||||
sessionInflight = undefined
|
||||
sessionAll = undefined
|
||||
return [] as Entry[]
|
||||
}
|
||||
|
||||
if (sessionAll) return sessionAll
|
||||
if (sessionInflight) return sessionInflight
|
||||
|
||||
const current = sessionToken.value
|
||||
const dirs = workspaces()
|
||||
if (dirs.length === 0) return [] as Entry[]
|
||||
|
||||
sessionInflight = Promise.all(
|
||||
dirs.map((directory) => {
|
||||
const description = label(directory)
|
||||
return globalSDK.client.session
|
||||
.list({ directory, roots: true })
|
||||
.then((x) =>
|
||||
(x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.map((s) => ({
|
||||
id: s.id,
|
||||
title: s.title ?? language.t("command.session.new"),
|
||||
description,
|
||||
directory,
|
||||
archived: s.time?.archived,
|
||||
})),
|
||||
)
|
||||
.catch(() => [] as { id: string; title: string; description: string; directory: string; archived?: number }[])
|
||||
}),
|
||||
)
|
||||
.then((results) => {
|
||||
if (sessionToken.value !== current) return [] as Entry[]
|
||||
const seen = new Set<string>()
|
||||
const next = results
|
||||
.flat()
|
||||
.filter((item) => {
|
||||
const key = `${item.directory}:${item.id}`
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
.map(sessionItem)
|
||||
sessionAll = next
|
||||
return next
|
||||
})
|
||||
.catch(() => [] as Entry[])
|
||||
.finally(() => {
|
||||
sessionInflight = undefined
|
||||
})
|
||||
|
||||
return sessionInflight
|
||||
}
|
||||
|
||||
const items = async (text: string) => {
|
||||
const query = text.trim()
|
||||
setGrouped(query.length > 0)
|
||||
@@ -146,9 +266,10 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
const files = await file.searchFiles(query)
|
||||
return files.map(fileItem)
|
||||
}
|
||||
const files = await file.searchFiles(query)
|
||||
|
||||
const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))])
|
||||
const entries = files.map(fileItem)
|
||||
return [...list(), ...entries]
|
||||
return [...list(), ...nextSessions, ...entries]
|
||||
}
|
||||
|
||||
const handleMove = (item: Entry | undefined) => {
|
||||
@@ -162,6 +283,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.open()
|
||||
layout.fileTree.setTab("all")
|
||||
props.onOpenFile?.(path)
|
||||
@@ -178,6 +300,12 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
return
|
||||
}
|
||||
|
||||
if (item.type === "session") {
|
||||
if (!item.directory || !item.sessionID) return
|
||||
navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!item.path) return
|
||||
open(item.path)
|
||||
}
|
||||
@@ -202,13 +330,12 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
items={items}
|
||||
key={(item) => item.id}
|
||||
filterKeys={["title", "description", "category"]}
|
||||
groupBy={(item) => item.category}
|
||||
groupBy={grouped() ? (item) => item.category : () => ""}
|
||||
onMove={handleMove}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{(item) => (
|
||||
<Show
|
||||
when={item.type === "command"}
|
||||
<Switch
|
||||
fallback={
|
||||
<div class="w-full flex items-center justify-between rounded-md pl-1">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
@@ -223,18 +350,43 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="w-full flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
||||
<Show when={item.description}>
|
||||
<span class="text-14-regular text-text-weak truncate">{item.description}</span>
|
||||
<Match when={item.type === "command"}>
|
||||
<div class="w-full flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
||||
<Show when={item.description}>
|
||||
<span class="text-14-regular text-text-weak truncate">{item.description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={item.keybind}>
|
||||
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={item.keybind}>
|
||||
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={item.type === "session"}>
|
||||
<div class="w-full flex items-center justify-between rounded-md pl-1">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<Icon name="bubble-5" size="small" class="shrink-0 text-icon-weak" />
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
class="text-14-regular text-text-strong truncate"
|
||||
classList={{ "opacity-70": !!item.archived }}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
<Show when={item.description}>
|
||||
<span
|
||||
class="text-14-regular text-text-weak truncate"
|
||||
classList={{ "opacity-70": !!item.archived }}
|
||||
>
|
||||
{item.description}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
|
||||
@@ -54,7 +54,6 @@ const ModelList: Component<{
|
||||
class="w-full"
|
||||
placement="right-start"
|
||||
gutter={12}
|
||||
forceMount={false}
|
||||
value={
|
||||
<ModelTooltip
|
||||
model={item}
|
||||
@@ -90,10 +89,9 @@ const ModelList: Component<{
|
||||
|
||||
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
||||
provider?: string
|
||||
children?: JSX.Element | ((open: boolean) => JSX.Element)
|
||||
children?: JSX.Element
|
||||
triggerAs?: T
|
||||
triggerProps?: ComponentProps<T>
|
||||
gutter?: number
|
||||
}) {
|
||||
const [store, setStore] = createStore<{
|
||||
open: boolean
|
||||
@@ -176,14 +174,14 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
||||
}}
|
||||
modal={false}
|
||||
placement="top-start"
|
||||
gutter={props.gutter ?? 8}
|
||||
gutter={8}
|
||||
>
|
||||
<Kobalte.Trigger
|
||||
ref={(el) => setStore("trigger", el)}
|
||||
as={props.triggerAs ?? "div"}
|
||||
{...(props.triggerProps as any)}
|
||||
>
|
||||
{typeof props.children === "function" ? props.children(store.open) : props.children}
|
||||
{props.children}
|
||||
</Kobalte.Trigger>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Content
|
||||
@@ -215,7 +213,7 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
||||
class="p-1"
|
||||
action={
|
||||
<div class="flex items-center gap-1">
|
||||
<Tooltip placement="top" forceMount={false} value={language.t("command.provider.connect")}>
|
||||
<Tooltip placement="top" value={language.t("command.provider.connect")}>
|
||||
<IconButton
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
@@ -225,7 +223,7 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
||||
onClick={handleConnectProvider}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" forceMount={false} value={language.t("dialog.model.manage")}>
|
||||
<Tooltip placement="top" value={language.t("dialog.model.manage")}>
|
||||
<IconButton
|
||||
icon="sliders"
|
||||
variant="ghost"
|
||||
|
||||
@@ -130,10 +130,57 @@ export default function FileTree(props: {
|
||||
const nodes = file.tree.children(props.path)
|
||||
const current = filter()
|
||||
if (!current) return nodes
|
||||
return nodes.filter((node) => {
|
||||
|
||||
const parent = (path: string) => {
|
||||
const idx = path.lastIndexOf("/")
|
||||
if (idx === -1) return ""
|
||||
return path.slice(0, idx)
|
||||
}
|
||||
|
||||
const leaf = (path: string) => {
|
||||
const idx = path.lastIndexOf("/")
|
||||
return idx === -1 ? path : path.slice(idx + 1)
|
||||
}
|
||||
|
||||
const out = nodes.filter((node) => {
|
||||
if (node.type === "file") return current.files.has(node.path)
|
||||
return current.dirs.has(node.path)
|
||||
})
|
||||
|
||||
const seen = new Set(out.map((node) => node.path))
|
||||
|
||||
for (const dir of current.dirs) {
|
||||
if (parent(dir) !== props.path) continue
|
||||
if (seen.has(dir)) continue
|
||||
out.push({
|
||||
name: leaf(dir),
|
||||
path: dir,
|
||||
absolute: dir,
|
||||
type: "directory",
|
||||
ignored: false,
|
||||
})
|
||||
seen.add(dir)
|
||||
}
|
||||
|
||||
for (const item of current.files) {
|
||||
if (parent(item) !== props.path) continue
|
||||
if (seen.has(item)) continue
|
||||
out.push({
|
||||
name: leaf(item),
|
||||
path: item,
|
||||
absolute: item,
|
||||
type: "file",
|
||||
ignored: false,
|
||||
})
|
||||
seen.add(item)
|
||||
}
|
||||
|
||||
return out.toSorted((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
return a.type === "directory" ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
})
|
||||
|
||||
const Node = (
|
||||
@@ -194,7 +241,7 @@ export default function FileTree(props: {
|
||||
: kind === "del"
|
||||
? "color: var(--icon-diff-delete-base)"
|
||||
: kind === "mix"
|
||||
? "color: var(--icon-diff-modified-base)"
|
||||
? "color: var(--icon-warning-active)"
|
||||
: undefined
|
||||
return (
|
||||
<span
|
||||
@@ -221,7 +268,7 @@ export default function FileTree(props: {
|
||||
? "color: var(--icon-diff-add-base)"
|
||||
: kind === "del"
|
||||
? "color: var(--icon-diff-delete-base)"
|
||||
: "color: var(--icon-diff-modified-base)"
|
||||
: "color: var(--icon-warning-active)"
|
||||
|
||||
return (
|
||||
<span class="shrink-0 w-4 text-center text-12-medium" style={color}>
|
||||
@@ -236,7 +283,7 @@ export default function FileTree(props: {
|
||||
? "background-color: var(--icon-diff-add-base)"
|
||||
: kind === "del"
|
||||
? "background-color: var(--icon-diff-delete-base)"
|
||||
: "background-color: var(--icon-diff-modified-base)"
|
||||
: "background-color: var(--icon-warning-active)"
|
||||
|
||||
return <div class="shrink-0 size-1.5 mr-1.5 rounded-full" style={color} />
|
||||
}
|
||||
@@ -274,7 +321,6 @@ export default function FileTree(props: {
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
forceMount={false}
|
||||
openDelay={2000}
|
||||
placement="bottom-start"
|
||||
class="w-full"
|
||||
|
||||
@@ -32,9 +32,7 @@ import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useComments } from "@/context/comments"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { MorphChevron } from "@opencode-ai/ui/morph-chevron"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { CycleLabel } from "@opencode-ai/ui/cycle-label"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
@@ -44,7 +42,6 @@ import { Select } from "@opencode-ai/ui/select"
|
||||
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/util/path"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||
import { ReasoningIcon } from "@opencode-ai/ui/reasoning-icon"
|
||||
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
||||
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
@@ -175,6 +172,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
|
||||
const commentInReview = (path: string) => {
|
||||
const sessionID = params.id
|
||||
@@ -193,12 +191,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path))
|
||||
if (wantsReview) {
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.open()
|
||||
layout.fileTree.setTab("changes")
|
||||
requestAnimationFrame(() => comments.setFocus(focus))
|
||||
return
|
||||
}
|
||||
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.open()
|
||||
layout.fileTree.setTab("all")
|
||||
const tab = files.tab(item.path)
|
||||
@@ -1135,7 +1135,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const images = imageAttachments().slice()
|
||||
const mode = store.mode
|
||||
|
||||
if (text.trim().length === 0 && images.length === 0) {
|
||||
if (text.trim().length === 0 && images.length === 0 && commentCount() === 0) {
|
||||
if (working()) abort()
|
||||
return
|
||||
}
|
||||
@@ -1223,7 +1223,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
})
|
||||
return undefined
|
||||
})
|
||||
if (session) navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
||||
if (session) {
|
||||
layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
|
||||
navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
||||
}
|
||||
}
|
||||
if (!session) return
|
||||
|
||||
@@ -1257,7 +1260,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
clearInput()
|
||||
client.session
|
||||
.shell({
|
||||
sessionID: session?.id || "",
|
||||
sessionID: session.id,
|
||||
agent,
|
||||
model,
|
||||
command: text,
|
||||
@@ -1280,7 +1283,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
clearInput()
|
||||
client.session
|
||||
.command({
|
||||
sessionID: session?.id || "",
|
||||
sessionID: session.id,
|
||||
command: commandName,
|
||||
arguments: args.join(" "),
|
||||
agent,
|
||||
@@ -1436,13 +1439,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
const optimisticParts = requestParts.map((part) => ({
|
||||
...part,
|
||||
sessionID: session?.id || "",
|
||||
sessionID: session.id,
|
||||
messageID,
|
||||
})) as unknown as Part[]
|
||||
|
||||
const optimisticMessage: Message = {
|
||||
id: messageID,
|
||||
sessionID: session?.id || "",
|
||||
sessionID: session.id,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent,
|
||||
@@ -1453,9 +1456,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
sync.set(
|
||||
produce((draft) => {
|
||||
const messages = draft.message[session?.id || ""]
|
||||
const messages = draft.message[session.id]
|
||||
if (!messages) {
|
||||
draft.message[session?.id || ""] = [optimisticMessage]
|
||||
draft.message[session.id] = [optimisticMessage]
|
||||
} else {
|
||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||
messages.splice(result.index, 0, optimisticMessage)
|
||||
@@ -1463,7 +1466,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
draft.part[messageID] = optimisticParts
|
||||
.filter((p) => !!p?.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
}),
|
||||
)
|
||||
return
|
||||
@@ -1471,9 +1474,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
globalSync.child(sessionDirectory)[1](
|
||||
produce((draft) => {
|
||||
const messages = draft.message[session?.id || ""]
|
||||
const messages = draft.message[session.id]
|
||||
if (!messages) {
|
||||
draft.message[session?.id || ""] = [optimisticMessage]
|
||||
draft.message[session.id] = [optimisticMessage]
|
||||
} else {
|
||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||
messages.splice(result.index, 0, optimisticMessage)
|
||||
@@ -1481,7 +1484,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
draft.part[messageID] = optimisticParts
|
||||
.filter((p) => !!p?.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1490,7 +1493,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
sync.set(
|
||||
produce((draft) => {
|
||||
const messages = draft.message[session?.id || ""]
|
||||
const messages = draft.message[session.id]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
@@ -1503,7 +1506,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
globalSync.child(sessionDirectory)[1](
|
||||
produce((draft) => {
|
||||
const messages = draft.message[session?.id || ""]
|
||||
const messages = draft.message[session.id]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
@@ -1524,15 +1527,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const worktree = WorktreeState.get(sessionDirectory)
|
||||
if (!worktree || worktree.status !== "pending") return true
|
||||
|
||||
if (sessionDirectory === projectDirectory && session?.id) {
|
||||
sync.set("session_status", session?.id, { type: "busy" })
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
sync.set("session_status", session.id, { type: "busy" })
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
|
||||
const cleanup = () => {
|
||||
if (sessionDirectory === projectDirectory && session?.id) {
|
||||
sync.set("session_status", session?.id, { type: "idle" })
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
sync.set("session_status", session.id, { type: "idle" })
|
||||
}
|
||||
removeOptimisticMessage()
|
||||
for (const item of commentItems) {
|
||||
@@ -1549,7 +1552,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
restoreInput()
|
||||
}
|
||||
|
||||
pending.set(session?.id || "", { abort: controller, cleanup })
|
||||
pending.set(session.id, { abort: controller, cleanup })
|
||||
|
||||
const abort = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||
if (controller.signal.aborted) {
|
||||
@@ -1577,7 +1580,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
if (timer.id === undefined) return
|
||||
clearTimeout(timer.id)
|
||||
})
|
||||
pending.delete(session?.id || "")
|
||||
pending.delete(session.id)
|
||||
if (controller.signal.aborted) return false
|
||||
if (result.status === "failed") throw new Error(result.message)
|
||||
return true
|
||||
@@ -1587,7 +1590,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const ok = await waitForWorktree()
|
||||
if (!ok) return
|
||||
await client.session.prompt({
|
||||
sessionID: session?.id || "",
|
||||
sessionID: session.id,
|
||||
agent,
|
||||
model,
|
||||
messageID,
|
||||
@@ -1597,9 +1600,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
|
||||
void send().catch((err) => {
|
||||
pending.delete(session?.id || "")
|
||||
if (sessionDirectory === projectDirectory && session?.id) {
|
||||
sync.set("session_status", session?.id, { type: "idle" })
|
||||
pending.delete(session.id)
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
sync.set("session_status", session.id, { type: "idle" })
|
||||
}
|
||||
showToast({
|
||||
title: language.t("prompt.toast.promptSendFailed.title"),
|
||||
@@ -1621,28 +1624,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const currrentModelVariant = createMemo(() => {
|
||||
const modelVariant = local.model.variant.current() ?? ""
|
||||
return modelVariant === "xhigh"
|
||||
? "xHigh"
|
||||
: modelVariant.length > 0
|
||||
? modelVariant[0].toUpperCase() + modelVariant.slice(1)
|
||||
: "Default"
|
||||
})
|
||||
|
||||
const reasoningPercentage = createMemo(() => {
|
||||
const variants = local.model.variant.list()
|
||||
const current = local.model.variant.current()
|
||||
const totalEntries = variants.length + 1
|
||||
|
||||
if (totalEntries <= 2 || current === "Default") {
|
||||
return 0
|
||||
}
|
||||
|
||||
const currentIndex = current ? variants.indexOf(current) + 1 : 0
|
||||
return ((currentIndex + 1) / totalEntries) * 100
|
||||
}, [local.model.variant])
|
||||
|
||||
return (
|
||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-3">
|
||||
<Show when={store.popover}>
|
||||
@@ -1695,7 +1676,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Icon name="brain" size="normal" class="text-icon-info-active shrink-0" />
|
||||
<Icon name="brain" size="small" class="text-icon-info-active shrink-0" />
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">
|
||||
@{(item as { type: "agent"; name: string }).name}
|
||||
</span>
|
||||
@@ -1760,9 +1741,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}}
|
||||
>
|
||||
<Show when={store.dragging}>
|
||||
<div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-raised-stronger-non-alpha/90 mr-1 pointer-events-none">
|
||||
<div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-raised-stronger-non-alpha/90 pointer-events-none">
|
||||
<div class="flex flex-col items-center gap-2 text-text-weak">
|
||||
<Icon name="photo" size={18} class="text-icon-base stroke-1.5" />
|
||||
<Icon name="photo" class="size-8" />
|
||||
<span class="text-14-regular">{language.t("prompt.dropzone.label")}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1801,7 +1782,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-7" />
|
||||
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
|
||||
<div class="flex items-center text-11-regular min-w-0 font-medium">
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilenameTruncated(item.path, 14)}</span>
|
||||
<Show when={item.selection}>
|
||||
@@ -1818,7 +1799,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
type="button"
|
||||
icon="close-small"
|
||||
variant="ghost"
|
||||
class="ml-auto size-7 opacity-0 group-hover:opacity-100 transition-all"
|
||||
class="ml-auto size-3.5 opacity-0 group-hover:opacity-100 transition-all"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (item.commentID) comments.remove(item.path, item.commentID)
|
||||
@@ -1848,7 +1829,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
when={attachment.mime.startsWith("image/")}
|
||||
fallback={
|
||||
<div class="size-16 rounded-md bg-surface-base flex items-center justify-center border border-border-base">
|
||||
<Icon name="folder" size="normal" class="size-6 text-text-base" />
|
||||
<Icon name="folder" class="size-6 text-text-weak" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -1921,8 +1902,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="relative p-3 flex items-center justify-between">
|
||||
<div class="flex items-center justify-start gap-2">
|
||||
<div class="relative p-3 flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Switch>
|
||||
<Match when={store.mode === "shell"}>
|
||||
<div class="flex items-center gap-2 px-2 h-6">
|
||||
@@ -1934,6 +1915,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<Match when={store.mode === "normal"}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
title={language.t("command.agent.cycle")}
|
||||
keybind={command.keybind("agent.cycle")}
|
||||
>
|
||||
@@ -1941,9 +1923,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
options={local.agent.list().map((agent) => agent.name)}
|
||||
current={local.agent.current()?.name ?? ""}
|
||||
onSelect={local.agent.set}
|
||||
class="capitalize"
|
||||
class={`capitalize ${local.model.variant.list().length > 0 ? "max-w-[80px]" : "max-w-[120px]"}`}
|
||||
valueClass="truncate"
|
||||
variant="ghost"
|
||||
gutter={12}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
<Show
|
||||
@@ -1951,66 +1933,68 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
fallback={
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<Button
|
||||
as="div"
|
||||
variant="ghost"
|
||||
class="px-2"
|
||||
class="px-2 min-w-0 max-w-[240px]"
|
||||
onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
||||
</Show>
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
<MorphChevron
|
||||
expanded={!!dialog.active?.id && dialog.active.id.startsWith("select-model-unpaid")}
|
||||
/>
|
||||
<span class="truncate">
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<ModelSelectorPopover triggerAs={Button} triggerProps={{ variant: "ghost" }} gutter={12}>
|
||||
{(open) => (
|
||||
<>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
||||
</Show>
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
<MorphChevron expanded={open} class="text-text-weak" />
|
||||
</>
|
||||
)}
|
||||
<ModelSelectorPopover
|
||||
triggerAs={Button}
|
||||
triggerProps={{ variant: "ghost", class: "min-w-0 max-w-[240px]" }}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
||||
</Show>
|
||||
<span class="truncate">
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||
</ModelSelectorPopover>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<Show when={local.model.variant.list().length > 0}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
title={language.t("command.model.variant.cycle")}
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
>
|
||||
<Button
|
||||
data-action="model-variant-cycle"
|
||||
variant="ghost"
|
||||
class="text-text-strong text-12-regular"
|
||||
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
|
||||
onClick={() => local.model.variant.cycle()}
|
||||
>
|
||||
<Show when={local.model.variant.list().length > 1}>
|
||||
<ReasoningIcon percentage={reasoningPercentage()} size={16} strokeWidth={1.25} />
|
||||
</Show>
|
||||
<CycleLabel value={currrentModelVariant()} />
|
||||
{local.model.variant.current() ?? language.t("common.default")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<Show when={permission.permissionsEnabled() && params.id}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
title={language.t("command.permissions.autoaccept.enable")}
|
||||
keybind={command.keybind("permissions.autoaccept")}
|
||||
>
|
||||
@@ -2018,7 +2002,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
variant="ghost"
|
||||
onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
|
||||
classList={{
|
||||
"_hidden group-hover/prompt-input:flex items-center justify-center": true,
|
||||
"_hidden group-hover/prompt-input:flex size-6 items-center justify-center": true,
|
||||
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
|
||||
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
|
||||
}}
|
||||
@@ -2040,7 +2024,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 absolute right-3 bottom-3">
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
@@ -2052,19 +2036,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
e.currentTarget.value = ""
|
||||
}}
|
||||
/>
|
||||
<div class="flex items-center gap-1.5 mr-1.5">
|
||||
<div class="flex items-center gap-1 mr-1">
|
||||
<SessionContextUsage />
|
||||
<Show when={store.mode === "normal"}>
|
||||
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
class="px-1"
|
||||
class="size-6 px-1"
|
||||
onClick={() => fileInputRef.click()}
|
||||
aria-label={language.t("prompt.action.attachFile")}
|
||||
>
|
||||
<Icon name="photo" class="size-6 text-icon-base" />
|
||||
<Icon name="photo" class="size-4.5" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
@@ -2083,7 +2066,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<Match when={true}>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{language.t("prompt.action.send")}</span>
|
||||
<Icon name="enter" size="normal" class="text-icon-base" />
|
||||
<Icon name="enter" size="small" class="text-icon-base" />
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -2091,10 +2074,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
>
|
||||
<IconButton
|
||||
type="submit"
|
||||
disabled={!prompt.dirty() && !working()}
|
||||
disabled={!prompt.dirty() && !working() && commentCount() === 0}
|
||||
icon={working() ? "stop" : "arrow-up"}
|
||||
variant="primary"
|
||||
class="h-6 w-5.5"
|
||||
class="h-6 w-4.5"
|
||||
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import { For, Show, createMemo, type Component } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
|
||||
export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => {
|
||||
const sdk = useSDK()
|
||||
const language = useLanguage()
|
||||
|
||||
const questions = createMemo(() => props.request.questions)
|
||||
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
tab: 0,
|
||||
answers: [] as QuestionAnswer[],
|
||||
custom: [] as string[],
|
||||
editing: false,
|
||||
sending: false,
|
||||
})
|
||||
|
||||
const question = createMemo(() => questions()[store.tab])
|
||||
const confirm = createMemo(() => !single() && store.tab === questions().length)
|
||||
const options = createMemo(() => question()?.options ?? [])
|
||||
const input = createMemo(() => store.custom[store.tab] ?? "")
|
||||
const multi = createMemo(() => question()?.multiple === true)
|
||||
const customPicked = createMemo(() => {
|
||||
const value = input()
|
||||
if (!value) return false
|
||||
return store.answers[store.tab]?.includes(value) ?? false
|
||||
})
|
||||
|
||||
const fail = (err: unknown) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
}
|
||||
|
||||
const reply = (answers: QuestionAnswer[]) => {
|
||||
if (store.sending) return
|
||||
|
||||
setStore("sending", true)
|
||||
sdk.client.question
|
||||
.reply({ requestID: props.request.id, answers })
|
||||
.catch(fail)
|
||||
.finally(() => setStore("sending", false))
|
||||
}
|
||||
|
||||
const reject = () => {
|
||||
if (store.sending) return
|
||||
|
||||
setStore("sending", true)
|
||||
sdk.client.question
|
||||
.reject({ requestID: props.request.id })
|
||||
.catch(fail)
|
||||
.finally(() => setStore("sending", false))
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
reply(questions().map((_, i) => store.answers[i] ?? []))
|
||||
}
|
||||
|
||||
const pick = (answer: string, custom: boolean = false) => {
|
||||
const answers = [...store.answers]
|
||||
answers[store.tab] = [answer]
|
||||
setStore("answers", answers)
|
||||
|
||||
if (custom) {
|
||||
const inputs = [...store.custom]
|
||||
inputs[store.tab] = answer
|
||||
setStore("custom", inputs)
|
||||
}
|
||||
|
||||
if (single()) {
|
||||
reply([[answer]])
|
||||
return
|
||||
}
|
||||
|
||||
setStore("tab", store.tab + 1)
|
||||
}
|
||||
|
||||
const toggle = (answer: string) => {
|
||||
const existing = store.answers[store.tab] ?? []
|
||||
const next = [...existing]
|
||||
const index = next.indexOf(answer)
|
||||
if (index === -1) next.push(answer)
|
||||
if (index !== -1) next.splice(index, 1)
|
||||
|
||||
const answers = [...store.answers]
|
||||
answers[store.tab] = next
|
||||
setStore("answers", answers)
|
||||
}
|
||||
|
||||
const selectTab = (index: number) => {
|
||||
setStore("tab", index)
|
||||
setStore("editing", false)
|
||||
}
|
||||
|
||||
const selectOption = (optIndex: number) => {
|
||||
if (store.sending) return
|
||||
|
||||
if (optIndex === options().length) {
|
||||
setStore("editing", true)
|
||||
return
|
||||
}
|
||||
|
||||
const opt = options()[optIndex]
|
||||
if (!opt) return
|
||||
if (multi()) {
|
||||
toggle(opt.label)
|
||||
return
|
||||
}
|
||||
pick(opt.label)
|
||||
}
|
||||
|
||||
const handleCustomSubmit = (e: Event) => {
|
||||
e.preventDefault()
|
||||
if (store.sending) return
|
||||
|
||||
const value = input().trim()
|
||||
if (!value) {
|
||||
setStore("editing", false)
|
||||
return
|
||||
}
|
||||
|
||||
if (multi()) {
|
||||
const existing = store.answers[store.tab] ?? []
|
||||
const next = [...existing]
|
||||
if (!next.includes(value)) next.push(value)
|
||||
|
||||
const answers = [...store.answers]
|
||||
answers[store.tab] = next
|
||||
setStore("answers", answers)
|
||||
setStore("editing", false)
|
||||
return
|
||||
}
|
||||
|
||||
pick(value, true)
|
||||
setStore("editing", false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-component="question-prompt">
|
||||
<Show when={!single()}>
|
||||
<div data-slot="question-tabs">
|
||||
<For each={questions()}>
|
||||
{(q, index) => {
|
||||
const active = () => index() === store.tab
|
||||
const answered = () => (store.answers[index()]?.length ?? 0) > 0
|
||||
return (
|
||||
<button
|
||||
data-slot="question-tab"
|
||||
data-active={active()}
|
||||
data-answered={answered()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectTab(index())}
|
||||
>
|
||||
{q.header}
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<button
|
||||
data-slot="question-tab"
|
||||
data-active={confirm()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectTab(questions().length)}
|
||||
>
|
||||
{language.t("ui.common.confirm")}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!confirm()}>
|
||||
<div data-slot="question-content">
|
||||
<div data-slot="question-text">
|
||||
{question()?.question}
|
||||
{multi() ? " " + language.t("ui.question.multiHint") : ""}
|
||||
</div>
|
||||
<div data-slot="question-options">
|
||||
<For each={options()}>
|
||||
{(opt, i) => {
|
||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||
return (
|
||||
<button
|
||||
data-slot="question-option"
|
||||
data-picked={picked()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectOption(i())}
|
||||
>
|
||||
<span data-slot="option-label">{opt.label}</span>
|
||||
<Show when={opt.description}>
|
||||
<span data-slot="option-description">{opt.description}</span>
|
||||
</Show>
|
||||
<Show when={picked()}>
|
||||
<Icon name="check-small" size="normal" />
|
||||
</Show>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<button
|
||||
data-slot="question-option"
|
||||
data-picked={customPicked()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectOption(options().length)}
|
||||
>
|
||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||
<Show when={!store.editing && input()}>
|
||||
<span data-slot="option-description">{input()}</span>
|
||||
</Show>
|
||||
<Show when={customPicked()}>
|
||||
<Icon name="check-small" size="normal" />
|
||||
</Show>
|
||||
</button>
|
||||
<Show when={store.editing}>
|
||||
<form data-slot="custom-input-form" onSubmit={handleCustomSubmit}>
|
||||
<input
|
||||
ref={(el) => setTimeout(() => el.focus(), 0)}
|
||||
type="text"
|
||||
data-slot="custom-input"
|
||||
placeholder={language.t("ui.question.custom.placeholder")}
|
||||
value={input()}
|
||||
disabled={store.sending}
|
||||
onInput={(e) => {
|
||||
const inputs = [...store.custom]
|
||||
inputs[store.tab] = e.currentTarget.value
|
||||
setStore("custom", inputs)
|
||||
}}
|
||||
/>
|
||||
<Button type="submit" variant="primary" size="small" disabled={store.sending}>
|
||||
{multi() ? language.t("ui.common.add") : language.t("ui.common.submit")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
disabled={store.sending}
|
||||
onClick={() => setStore("editing", false)}
|
||||
>
|
||||
{language.t("ui.common.cancel")}
|
||||
</Button>
|
||||
</form>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={confirm()}>
|
||||
<div data-slot="question-review">
|
||||
<div data-slot="review-title">{language.t("ui.messagePart.review.title")}</div>
|
||||
<For each={questions()}>
|
||||
{(q, index) => {
|
||||
const value = () => store.answers[index()]?.join(", ") ?? ""
|
||||
const answered = () => Boolean(value())
|
||||
return (
|
||||
<div data-slot="review-item">
|
||||
<span data-slot="review-label">{q.question}</span>
|
||||
<span data-slot="review-value" data-answered={answered()}>
|
||||
{answered() ? value() : language.t("ui.question.review.notAnswered")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div data-slot="question-actions">
|
||||
<Button variant="ghost" size="small" onClick={reject} disabled={store.sending}>
|
||||
{language.t("ui.common.dismiss")}
|
||||
</Button>
|
||||
<Show when={!single()}>
|
||||
<Show when={confirm()}>
|
||||
<Button variant="primary" size="small" onClick={submit} disabled={store.sending}>
|
||||
{language.t("ui.common.submit")}
|
||||
</Button>
|
||||
</Show>
|
||||
<Show when={!confirm() && multi()}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => selectTab(store.tab + 1)}
|
||||
disabled={store.sending || (store.answers[store.tab]?.length ?? 0) === 0}
|
||||
>
|
||||
{language.t("ui.common.next")}
|
||||
</Button>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
const variant = createMemo(() => props.variant ?? "button")
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
|
||||
const usd = createMemo(
|
||||
@@ -57,6 +58,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
|
||||
const openContext = () => {
|
||||
if (!params.id) return
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.open()
|
||||
layout.fileTree.setTab("all")
|
||||
tabs().open("context")
|
||||
@@ -64,7 +66,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
}
|
||||
|
||||
const circle = () => (
|
||||
<div class="p-1">
|
||||
<div class="flex items-center justify-center">
|
||||
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -6,18 +6,23 @@ import { useLayout } from "@/context/layout"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { AppIcon } from "@opencode-ai/ui/app-icon"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { Popover } from "@opencode-ai/ui/popover"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { StatusPopover } from "../status-popover"
|
||||
|
||||
export function SessionHeader() {
|
||||
@@ -25,6 +30,7 @@ export function SessionHeader() {
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const command = useCommand()
|
||||
const server = useServer()
|
||||
const sync = useSync()
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
@@ -48,6 +54,117 @@ export function SessionHeader() {
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
|
||||
const OPEN_APPS = [
|
||||
"vscode",
|
||||
"cursor",
|
||||
"zed",
|
||||
"textmate",
|
||||
"antigravity",
|
||||
"finder",
|
||||
"terminal",
|
||||
"iterm2",
|
||||
"ghostty",
|
||||
"xcode",
|
||||
"android-studio",
|
||||
"powershell",
|
||||
] as const
|
||||
type OpenApp = (typeof OPEN_APPS)[number]
|
||||
|
||||
const os = createMemo<"macos" | "windows" | "linux" | "unknown">(() => {
|
||||
if (platform.platform === "desktop" && platform.os) return platform.os
|
||||
if (typeof navigator !== "object") return "unknown"
|
||||
const value = navigator.platform || navigator.userAgent
|
||||
if (/Mac/i.test(value)) return "macos"
|
||||
if (/Win/i.test(value)) return "windows"
|
||||
if (/Linux/i.test(value)) return "linux"
|
||||
return "unknown"
|
||||
})
|
||||
|
||||
const options = createMemo(() => {
|
||||
if (os() === "macos") {
|
||||
return [
|
||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "Visual Studio Code" },
|
||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" },
|
||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "Zed" },
|
||||
{ id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" },
|
||||
{ id: "antigravity", label: "Antigravity", icon: "antigravity", openWith: "Antigravity" },
|
||||
{ id: "finder", label: "Finder", icon: "finder" },
|
||||
{ id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" },
|
||||
{ id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" },
|
||||
{ id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" },
|
||||
{ id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" },
|
||||
{ id: "android-studio", label: "Android Studio", icon: "android-studio", openWith: "Android Studio" },
|
||||
] as const
|
||||
}
|
||||
|
||||
if (os() === "windows") {
|
||||
return [
|
||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
|
||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
|
||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
|
||||
{ id: "finder", label: "File Explorer", icon: "finder" },
|
||||
{ id: "powershell", label: "PowerShell", icon: "powershell", openWith: "powershell" },
|
||||
] as const
|
||||
}
|
||||
|
||||
return [
|
||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
|
||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
|
||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
|
||||
{ id: "finder", label: "File Manager", icon: "finder" },
|
||||
] as const
|
||||
})
|
||||
|
||||
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
|
||||
|
||||
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
||||
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
|
||||
|
||||
createEffect(() => {
|
||||
if (platform.platform !== "desktop") return
|
||||
const value = prefs.app
|
||||
if (options().some((o) => o.id === value)) return
|
||||
setPrefs("app", options()[0]?.id ?? "finder")
|
||||
})
|
||||
|
||||
const openDir = (app: OpenApp) => {
|
||||
const directory = projectDirectory()
|
||||
if (!directory) return
|
||||
if (!canOpen()) return
|
||||
|
||||
const item = options().find((o) => o.id === app)
|
||||
const openWith = item && "openWith" in item ? item.openWith : undefined
|
||||
Promise.resolve(platform.openPath?.(directory, openWith)).catch((err: unknown) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const copyPath = () => {
|
||||
const directory = projectDirectory()
|
||||
if (!directory) return
|
||||
navigator.clipboard
|
||||
.writeText(directory)
|
||||
.then(() => {
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: language.t("session.share.copy.copied"),
|
||||
description: directory,
|
||||
})
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const [state, setState] = createStore({
|
||||
share: false,
|
||||
unshare: false,
|
||||
@@ -150,6 +267,76 @@ export function SessionHeader() {
|
||||
{(mount) => (
|
||||
<Portal mount={mount()}>
|
||||
<div class="flex items-center gap-3">
|
||||
<Show when={projectDirectory()}>
|
||||
<Show
|
||||
when={canOpen()}
|
||||
fallback={
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="rounded-sm h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none"
|
||||
onClick={copyPath}
|
||||
aria-label={language.t("session.header.open.copyPath")}
|
||||
>
|
||||
<Icon name="copy" size="small" class="text-icon-base" />
|
||||
<span class="text-12-regular text-text-strong">{language.t("session.header.open.copyPath")}</span>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="rounded-sm h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none rounded-r-none"
|
||||
onClick={() => openDir(current().id)}
|
||||
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
|
||||
>
|
||||
<AppIcon id={current().icon} class="size-5" />
|
||||
<span class="text-12-regular text-text-strong">
|
||||
{language.t("session.header.open.action", { app: current().label })}
|
||||
</span>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="chevron-down"
|
||||
variant="ghost"
|
||||
class="rounded-sm h-[24px] w-auto px-1.5 border-none shadow-none rounded-l-none data-[expanded]:bg-surface-raised-base-active"
|
||||
aria-label={language.t("session.header.open.menu")}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content placement="bottom-end" gutter={6}>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={prefs.app}
|
||||
onChange={(value) => {
|
||||
if (!OPEN_APPS.includes(value as OpenApp)) return
|
||||
setPrefs("app", value as OpenApp)
|
||||
}}
|
||||
>
|
||||
{options().map((o) => (
|
||||
<DropdownMenu.RadioItem value={o.id} onSelect={() => openDir(o.id)}>
|
||||
<AppIcon id={o.icon} class="size-5" />
|
||||
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
|
||||
<DropdownMenu.ItemIndicator>
|
||||
<Icon name="check-small" size="small" class="text-icon-weak" />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</DropdownMenu.RadioItem>
|
||||
))}
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={copyPath}>
|
||||
<Icon name="copy" size="small" class="text-icon-weak" />
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("session.header.open.copyPath")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
<StatusPopover />
|
||||
<Show when={showShare()}>
|
||||
<div class="flex items-center">
|
||||
@@ -283,27 +470,57 @@ export function SessionHeader() {
|
||||
<TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/file-tree-toggle size-6 p-0"
|
||||
onClick={() => layout.fileTree.toggle()}
|
||||
class="group/review-toggle size-6 p-0"
|
||||
onClick={() => view().reviewPanel.toggle()}
|
||||
aria-label={language.t("command.review.toggle")}
|
||||
aria-expanded={layout.fileTree.opened()}
|
||||
aria-expanded={view().reviewPanel.opened()}
|
||||
aria-controls="review-panel"
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.fileTree.opened() ? "layout-right-full" : "layout-right"}
|
||||
class="group-hover/file-tree-toggle:hidden"
|
||||
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-right"}
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-right-partial"
|
||||
class="hidden group-hover/file-tree-toggle:inline-block"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.fileTree.opened() ? "layout-right" : "layout-right-full"}
|
||||
class="hidden group-active/file-tree-toggle:inline-block"
|
||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-right-full"}
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<div class="hidden md:block shrink-0">
|
||||
<TooltipKeybind
|
||||
title={language.t("command.fileTree.toggle")}
|
||||
keybind={command.keybind("fileTree.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/file-tree-toggle size-6 p-0"
|
||||
onClick={() => {
|
||||
const opening = !layout.fileTree.opened()
|
||||
if (opening && !view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.toggle()
|
||||
}}
|
||||
aria-label={language.t("command.fileTree.toggle")}
|
||||
aria-expanded={layout.fileTree.opened()}
|
||||
aria-controls="file-tree-panel"
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4">
|
||||
<Icon
|
||||
size="small"
|
||||
name="bullet-list"
|
||||
classList={{
|
||||
"text-icon-strong": layout.fileTree.opened(),
|
||||
"text-icon-weak": !layout.fileTree.opened(),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Select } from "@opencode-ai/ui/select"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSettings, monoFontFamily } from "@/context/settings"
|
||||
@@ -131,12 +130,7 @@ export const SettingsGeneral: Component = () => {
|
||||
const soundOptions = [...SOUND_OPTIONS]
|
||||
|
||||
return (
|
||||
<ScrollFade
|
||||
direction="vertical"
|
||||
fadeStartSize={0}
|
||||
fadeEndSize={16}
|
||||
class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"
|
||||
>
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||
<div class="flex flex-col gap-1 pt-6 pb-8">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
|
||||
@@ -232,7 +226,7 @@ export const SettingsGeneral: Component = () => {
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "field-sizing": "content" }}
|
||||
triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "min-width": "180px" }}
|
||||
>
|
||||
{(option) => (
|
||||
<span style={{ "font-family": monoFontFamily(option?.value) }}>
|
||||
@@ -417,7 +411,7 @@ export const SettingsGeneral: Component = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollFade>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -353,12 +352,7 @@ export const SettingsKeybinds: Component = () => {
|
||||
})
|
||||
|
||||
return (
|
||||
<ScrollFade
|
||||
direction="vertical"
|
||||
fadeStartSize={0}
|
||||
fadeEndSize={16}
|
||||
class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"
|
||||
>
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
@@ -436,6 +430,6 @@ export const SettingsKeybinds: Component = () => {
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</ScrollFade>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { type Component, For, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useModels } from "@/context/models"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
|
||||
|
||||
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
|
||||
|
||||
@@ -40,12 +39,7 @@ export const SettingsModels: Component = () => {
|
||||
})
|
||||
|
||||
return (
|
||||
<ScrollFade
|
||||
direction="vertical"
|
||||
fadeStartSize={0}
|
||||
fadeEndSize={16}
|
||||
class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"
|
||||
>
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
||||
@@ -131,6 +125,6 @@ export const SettingsModels: Component = () => {
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</ScrollFade>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { useGlobalSync } from "@/context/global-sync"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
||||
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
|
||||
|
||||
type ProviderSource = "env" | "api" | "config" | "custom"
|
||||
type ProviderMeta = { source?: ProviderSource }
|
||||
@@ -116,12 +115,7 @@ export const SettingsProviders: Component = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollFade
|
||||
direction="vertical"
|
||||
fadeStartSize={0}
|
||||
fadeEndSize={16}
|
||||
class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"
|
||||
>
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
||||
@@ -232,11 +226,11 @@ export const SettingsProviders: Component = () => {
|
||||
</For>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between gap-4 h-16 border-b border-border-weak-base last:border-none"
|
||||
class="flex items-center justify-between gap-4 min-h-16 border-b border-border-weak-base last:border-none flex-wrap py-3"
|
||||
data-component="custom-provider-section"
|
||||
>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<ProviderIcon id={icon("synthetic")} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong">Custom provider</span>
|
||||
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
||||
@@ -267,6 +261,6 @@ export const SettingsProviders: Component = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollFade>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
|
||||
import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { monoFontFamily, useSettings } from "@/context/settings"
|
||||
import { SerializeAddon } from "@/addons/serialize"
|
||||
@@ -52,6 +53,7 @@ const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
|
||||
}
|
||||
|
||||
export const Terminal = (props: TerminalProps) => {
|
||||
const platform = usePlatform()
|
||||
const sdk = useSDK()
|
||||
const settings = useSettings()
|
||||
const theme = useTheme()
|
||||
@@ -68,6 +70,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
let handleTextareaBlur: () => void
|
||||
let disposed = false
|
||||
const cleanups: VoidFunction[] = []
|
||||
let tail = local.pty.tail ?? ""
|
||||
|
||||
const cleanup = () => {
|
||||
if (!cleanups.length) return
|
||||
@@ -135,6 +138,22 @@ export const Terminal = (props: TerminalProps) => {
|
||||
focusTerminal()
|
||||
}
|
||||
|
||||
const handleLinkClick = (event: MouseEvent) => {
|
||||
if (!event.shiftKey && !event.ctrlKey && !event.metaKey) return
|
||||
if (event.altKey) return
|
||||
if (event.button !== 0) return
|
||||
|
||||
const t = term
|
||||
if (!t) return
|
||||
|
||||
const link = (t as unknown as { currentHoveredLink?: { text: string } }).currentHoveredLink
|
||||
if (!link?.text) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
platform.openLink(link.text)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const run = async () => {
|
||||
const loaded = await loadGhostty()
|
||||
@@ -146,6 +165,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
const once = { value: false }
|
||||
|
||||
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
|
||||
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
||||
if (window.__OPENCODE__?.serverPassword) {
|
||||
url.username = "opencode"
|
||||
url.password = window.__OPENCODE__?.serverPassword
|
||||
@@ -166,6 +186,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
fontSize: 14,
|
||||
fontFamily: monoFontFamily(settings.appearance.font()),
|
||||
allowTransparency: true,
|
||||
convertEol: true,
|
||||
theme: terminalColors(),
|
||||
scrollback: 10_000,
|
||||
ghostty: g,
|
||||
@@ -236,9 +257,13 @@ export const Terminal = (props: TerminalProps) => {
|
||||
serializeAddon = serializer
|
||||
|
||||
t.open(container)
|
||||
|
||||
container.addEventListener("pointerdown", handlePointerDown)
|
||||
cleanups.push(() => container.removeEventListener("pointerdown", handlePointerDown))
|
||||
|
||||
container.addEventListener("click", handleLinkClick, { capture: true })
|
||||
cleanups.push(() => container.removeEventListener("click", handleLinkClick, { capture: true }))
|
||||
|
||||
handleTextareaFocus = () => {
|
||||
t.options.cursorBlink = true
|
||||
}
|
||||
@@ -253,15 +278,11 @@ export const Terminal = (props: TerminalProps) => {
|
||||
|
||||
focusTerminal()
|
||||
|
||||
fit.fit()
|
||||
|
||||
if (local.pty.buffer) {
|
||||
if (local.pty.rows && local.pty.cols) {
|
||||
t.resize(local.pty.cols, local.pty.rows)
|
||||
}
|
||||
t.write(local.pty.buffer, () => {
|
||||
if (local.pty.scrollY) {
|
||||
t.scrollToLine(local.pty.scrollY)
|
||||
}
|
||||
fitAddon.fit()
|
||||
if (local.pty.scrollY) t.scrollToLine(local.pty.scrollY)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -299,6 +320,19 @@ export const Terminal = (props: TerminalProps) => {
|
||||
// console.log("Scroll position:", ydisp)
|
||||
// })
|
||||
|
||||
const limit = 16_384
|
||||
const seed = tail
|
||||
let sync = !!seed
|
||||
|
||||
const overlap = (data: string) => {
|
||||
if (!seed) return 0
|
||||
const max = Math.min(seed.length, data.length)
|
||||
for (let i = max; i > 0; i--) {
|
||||
if (seed.slice(-i) === data.slice(0, i)) return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
local.onConnect?.()
|
||||
sdk.client.pty
|
||||
@@ -315,7 +349,25 @@ export const Terminal = (props: TerminalProps) => {
|
||||
cleanups.push(() => socket.removeEventListener("open", handleOpen))
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
t.write(event.data)
|
||||
const data = typeof event.data === "string" ? event.data : ""
|
||||
if (!data) return
|
||||
|
||||
const next = (() => {
|
||||
if (!sync) return data
|
||||
const n = overlap(data)
|
||||
if (!n) {
|
||||
sync = false
|
||||
return data
|
||||
}
|
||||
const trimmed = data.slice(n)
|
||||
if (trimmed) sync = false
|
||||
return trimmed
|
||||
})()
|
||||
|
||||
if (!next) return
|
||||
|
||||
t.write(next)
|
||||
tail = next.length >= limit ? next.slice(-limit) : (tail + next).slice(-limit)
|
||||
}
|
||||
socket.addEventListener("message", handleMessage)
|
||||
cleanups.push(() => socket.removeEventListener("message", handleMessage))
|
||||
@@ -369,6 +421,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
props.onCleanup({
|
||||
...local.pty,
|
||||
buffer,
|
||||
tail,
|
||||
rows: t.rows,
|
||||
cols: t.cols,
|
||||
scrollY: t.getViewportY(),
|
||||
|
||||
@@ -137,7 +137,6 @@ export function Titlebar() {
|
||||
<header
|
||||
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center"
|
||||
style={{ "min-height": minHeight() }}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
@@ -145,10 +144,9 @@ export function Titlebar() {
|
||||
"pl-2": !mac(),
|
||||
}}
|
||||
onMouseDown={drag}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<Show when={mac()}>
|
||||
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} data-tauri-drag-region />
|
||||
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
|
||||
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
|
||||
<IconButton
|
||||
icon="menu"
|
||||
@@ -222,13 +220,10 @@ export function Titlebar() {
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" data-tauri-drag-region />
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="min-w-0 flex items-center justify-center pointer-events-none lg:absolute lg:inset-0 lg:flex lg:items-center lg:justify-center"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div class="min-w-0 flex items-center justify-center pointer-events-none lg:absolute lg:inset-0 lg:flex lg:items-center lg:justify-center">
|
||||
<div id="opencode-titlebar-center" class="pointer-events-auto w-full min-w-0 flex justify-center lg:w-fit" />
|
||||
</div>
|
||||
|
||||
@@ -238,9 +233,8 @@ export function Titlebar() {
|
||||
"pr-6": !windows(),
|
||||
}}
|
||||
onMouseDown={drag}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0 justify-end" data-tauri-drag-region />
|
||||
<div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0 justify-end" />
|
||||
<Show when={windows()}>
|
||||
<div class="w-6 shrink-0" />
|
||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||
|
||||
@@ -70,6 +70,14 @@ function createCommentSession(dir: string, id: string | undefined) {
|
||||
setFocus((current) => (current?.id === id ? null : current))
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
batch(() => {
|
||||
setStore("comments", {})
|
||||
setFocus(null)
|
||||
setActive(null)
|
||||
})
|
||||
}
|
||||
|
||||
const all = createMemo(() => {
|
||||
const files = Object.keys(store.comments)
|
||||
const items = files.flatMap((file) => store.comments[file] ?? [])
|
||||
@@ -82,6 +90,7 @@ function createCommentSession(dir: string, id: string | undefined) {
|
||||
all,
|
||||
add,
|
||||
remove,
|
||||
clear,
|
||||
focus: createMemo(() => state.focus),
|
||||
setFocus,
|
||||
clearFocus: () => setFocus(null),
|
||||
@@ -144,6 +153,7 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont
|
||||
all: () => session().all(),
|
||||
add: (input: Omit<LineComment, "id" | "time">) => session().add(input),
|
||||
remove: (file: string, id: string) => session().remove(file, id),
|
||||
clear: () => session().clear(),
|
||||
focus: () => session().focus(),
|
||||
setFocus: (focus: CommentFocus | null) => session().setFocus(focus),
|
||||
clearFocus: () => session().clearFocus(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createEffect, createMemo, createRoot, onCleanup } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import type { FileContent, FileNode } from "@opencode-ai/sdk/v2"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
@@ -277,10 +277,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
|
||||
const scope = createMemo(() => sdk.directory)
|
||||
|
||||
const directory = createMemo(() => sync.data.path.directory)
|
||||
|
||||
function normalize(input: string) {
|
||||
const root = directory()
|
||||
const root = scope()
|
||||
const prefix = root.endsWith("/") ? root : root + "/"
|
||||
|
||||
let path = unquoteGitPath(stripQueryAndHash(stripFileProtocol(input)))
|
||||
@@ -371,9 +369,13 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
inflight.clear()
|
||||
treeInflight.clear()
|
||||
contentLru.clear()
|
||||
setStore("file", {})
|
||||
setTree("node", {})
|
||||
setTree("dir", { "": { expanded: true } })
|
||||
|
||||
batch(() => {
|
||||
setStore("file", reconcile({}))
|
||||
setTree("node", reconcile({}))
|
||||
setTree("dir", reconcile({}))
|
||||
setTree("dir", "", { expanded: true })
|
||||
})
|
||||
})
|
||||
|
||||
const viewCache = new Map<string, ViewCacheEntry>()
|
||||
@@ -414,7 +416,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
return entry.value
|
||||
}
|
||||
|
||||
const view = createMemo(() => loadView(params.dir!, params.id))
|
||||
const view = createMemo(() => loadView(scope(), params.id))
|
||||
|
||||
function ensure(path: string) {
|
||||
if (!path) return
|
||||
|
||||
@@ -119,6 +119,8 @@ type ChildOptions = {
|
||||
bootstrap?: boolean
|
||||
}
|
||||
|
||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
|
||||
function normalizeProviderList(input: ProviderListResponse): ProviderListResponse {
|
||||
return {
|
||||
...input,
|
||||
@@ -297,7 +299,7 @@ function createGlobalSync() {
|
||||
const aUpdated = sessionUpdatedAt(a)
|
||||
const bUpdated = sessionUpdatedAt(b)
|
||||
if (aUpdated !== bUpdated) return bUpdated - aUpdated
|
||||
return a.id.localeCompare(b.id)
|
||||
return cmp(a.id, b.id)
|
||||
}
|
||||
|
||||
function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) {
|
||||
@@ -325,7 +327,7 @@ function createGlobalSync() {
|
||||
const all = input
|
||||
.filter((s) => !!s?.id)
|
||||
.filter((s) => !s.time?.archived)
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
|
||||
const roots = all.filter((s) => !s.parentID)
|
||||
const children = all.filter((s) => !!s.parentID)
|
||||
@@ -342,7 +344,7 @@ function createGlobalSync() {
|
||||
return sessionUpdatedAt(s) > cutoff
|
||||
})
|
||||
|
||||
return [...keepRoots, ...keepChildren].sort((a, b) => a.id.localeCompare(b.id))
|
||||
return [...keepRoots, ...keepChildren].sort((a, b) => cmp(a.id, b.id))
|
||||
}
|
||||
|
||||
function ensureChild(directory: string) {
|
||||
@@ -457,7 +459,7 @@ function createGlobalSync() {
|
||||
const nonArchived = (x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.filter((s) => !s.time?.archived)
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
|
||||
// Read the current limit at resolve-time so callers that bump the limit while
|
||||
// a request is in-flight still get the expanded result.
|
||||
@@ -559,7 +561,7 @@ function createGlobalSync() {
|
||||
"permission",
|
||||
sessionID,
|
||||
reconcile(
|
||||
permissions.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id)),
|
||||
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
@@ -588,7 +590,7 @@ function createGlobalSync() {
|
||||
"question",
|
||||
sessionID,
|
||||
reconcile(
|
||||
questions.filter((q) => !!q?.id).sort((a, b) => a.id.localeCompare(b.id)),
|
||||
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
@@ -986,7 +988,7 @@ function createGlobalSync() {
|
||||
.filter((p) => !!p?.id)
|
||||
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
setGlobalStore("project", projects)
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -18,6 +18,7 @@ import { dict as ar } from "@/i18n/ar"
|
||||
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 uiEn } from "@opencode-ai/ui/i18n/en"
|
||||
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
|
||||
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht"
|
||||
@@ -33,6 +34,7 @@ import { dict as uiAr } from "@opencode-ai/ui/i18n/ar"
|
||||
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"
|
||||
|
||||
export type Locale =
|
||||
| "en"
|
||||
@@ -50,6 +52,7 @@ export type Locale =
|
||||
| "no"
|
||||
| "br"
|
||||
| "th"
|
||||
| "bs"
|
||||
|
||||
type RawDictionary = typeof en & typeof uiEn
|
||||
type Dictionary = i18n.Flatten<RawDictionary>
|
||||
@@ -66,6 +69,7 @@ const LOCALES: readonly Locale[] = [
|
||||
"ja",
|
||||
"pl",
|
||||
"ru",
|
||||
"bs",
|
||||
"ar",
|
||||
"no",
|
||||
"br",
|
||||
@@ -99,6 +103,7 @@ function detectLocale(): Locale {
|
||||
return "no"
|
||||
if (language.toLowerCase().startsWith("pt")) return "br"
|
||||
if (language.toLowerCase().startsWith("th")) return "th"
|
||||
if (language.toLowerCase().startsWith("bs")) return "bs"
|
||||
}
|
||||
|
||||
return "en"
|
||||
@@ -129,6 +134,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
if (store.locale === "no") return "no"
|
||||
if (store.locale === "br") return "br"
|
||||
if (store.locale === "th") return "th"
|
||||
if (store.locale === "bs") return "bs"
|
||||
return "en"
|
||||
})
|
||||
|
||||
@@ -154,6 +160,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) }
|
||||
if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) }
|
||||
if (locale() === "th") return { ...base, ...i18n.flatten({ ...th, ...uiTh }) }
|
||||
if (locale() === "bs") return { ...base, ...i18n.flatten({ ...bs, ...uiBs }) }
|
||||
return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }
|
||||
})
|
||||
|
||||
@@ -175,6 +182,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
no: "language.no",
|
||||
br: "language.br",
|
||||
th: "language.th",
|
||||
bs: "language.bs",
|
||||
}
|
||||
|
||||
const label = (value: Locale) => t(labelKey[value])
|
||||
|
||||
@@ -33,6 +33,14 @@ type SessionTabs = {
|
||||
type SessionView = {
|
||||
scroll: Record<string, SessionScroll>
|
||||
reviewOpen?: string[]
|
||||
pendingMessage?: string
|
||||
pendingMessageAt?: number
|
||||
}
|
||||
|
||||
type TabHandoff = {
|
||||
dir: string
|
||||
id: string
|
||||
at: number
|
||||
}
|
||||
|
||||
export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
|
||||
@@ -63,6 +71,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
})()
|
||||
|
||||
const review = value.review
|
||||
const fileTree = value.fileTree
|
||||
const migratedFileTree = (() => {
|
||||
if (!isRecord(fileTree)) return fileTree
|
||||
@@ -77,10 +86,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
})()
|
||||
|
||||
if (migratedSidebar === sidebar && migratedFileTree === fileTree) return value
|
||||
const migratedReview = (() => {
|
||||
if (!isRecord(review)) return review
|
||||
if (typeof review.panelOpened === "boolean") return review
|
||||
|
||||
const opened = isRecord(fileTree) && typeof fileTree.opened === "boolean" ? fileTree.opened : true
|
||||
return {
|
||||
...review,
|
||||
panelOpened: opened,
|
||||
}
|
||||
})()
|
||||
|
||||
if (migratedSidebar === sidebar && migratedReview === review && migratedFileTree === fileTree) return value
|
||||
return {
|
||||
...value,
|
||||
sidebar: migratedSidebar,
|
||||
review: migratedReview,
|
||||
fileTree: migratedFileTree,
|
||||
}
|
||||
}
|
||||
@@ -101,6 +122,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
},
|
||||
review: {
|
||||
diffStyle: "split" as ReviewDiffStyle,
|
||||
panelOpened: true,
|
||||
},
|
||||
fileTree: {
|
||||
opened: true,
|
||||
@@ -115,10 +137,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
},
|
||||
sessionTabs: {} as Record<string, SessionTabs>,
|
||||
sessionView: {} as Record<string, SessionView>,
|
||||
handoff: {
|
||||
tabs: undefined as TabHandoff | undefined,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const MAX_SESSION_KEYS = 50
|
||||
const PENDING_MESSAGE_TTL_MS = 2 * 60 * 1000
|
||||
const meta = { active: undefined as string | undefined, pruned: false }
|
||||
const used = new Map<string, number>()
|
||||
|
||||
@@ -411,6 +437,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
|
||||
return {
|
||||
ready,
|
||||
handoff: {
|
||||
tabs: createMemo(() => store.handoff?.tabs),
|
||||
setTabs(dir: string, id: string) {
|
||||
setStore("handoff", "tabs", { dir, id, at: Date.now() })
|
||||
},
|
||||
clearTabs() {
|
||||
if (!store.handoff?.tabs) return
|
||||
setStore("handoff", "tabs", undefined)
|
||||
},
|
||||
},
|
||||
projects: {
|
||||
list,
|
||||
open(directory: string) {
|
||||
@@ -468,7 +504,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
diffStyle: createMemo(() => store.review?.diffStyle ?? "split"),
|
||||
setDiffStyle(diffStyle: ReviewDiffStyle) {
|
||||
if (!store.review) {
|
||||
setStore("review", { diffStyle })
|
||||
setStore("review", { diffStyle, panelOpened: true })
|
||||
return
|
||||
}
|
||||
setStore("review", "diffStyle", diffStyle)
|
||||
@@ -536,6 +572,49 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
setStore("mobileSidebar", "opened", (x) => !x)
|
||||
},
|
||||
},
|
||||
pendingMessage: {
|
||||
set(sessionKey: string, messageID: string) {
|
||||
const at = Date.now()
|
||||
touch(sessionKey)
|
||||
const current = store.sessionView[sessionKey]
|
||||
if (!current) {
|
||||
setStore("sessionView", sessionKey, {
|
||||
scroll: {},
|
||||
pendingMessage: messageID,
|
||||
pendingMessageAt: at,
|
||||
})
|
||||
prune(meta.active ?? sessionKey)
|
||||
return
|
||||
}
|
||||
|
||||
setStore(
|
||||
"sessionView",
|
||||
sessionKey,
|
||||
produce((draft) => {
|
||||
draft.pendingMessage = messageID
|
||||
draft.pendingMessageAt = at
|
||||
}),
|
||||
)
|
||||
},
|
||||
consume(sessionKey: string) {
|
||||
const current = store.sessionView[sessionKey]
|
||||
const message = current?.pendingMessage
|
||||
const at = current?.pendingMessageAt
|
||||
if (!message || !at) return
|
||||
|
||||
setStore(
|
||||
"sessionView",
|
||||
sessionKey,
|
||||
produce((draft) => {
|
||||
delete draft.pendingMessage
|
||||
delete draft.pendingMessageAt
|
||||
}),
|
||||
)
|
||||
|
||||
if (Date.now() - at > PENDING_MESSAGE_TTL_MS) return
|
||||
return message
|
||||
},
|
||||
},
|
||||
view(sessionKey: string | Accessor<string>) {
|
||||
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
|
||||
|
||||
@@ -555,6 +634,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
|
||||
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
|
||||
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
|
||||
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
|
||||
|
||||
function setTerminalOpened(next: boolean) {
|
||||
const current = store.terminal
|
||||
@@ -568,6 +648,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
setStore("terminal", "opened", next)
|
||||
}
|
||||
|
||||
function setReviewPanelOpened(next: boolean) {
|
||||
const current = store.review
|
||||
if (!current) {
|
||||
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next })
|
||||
return
|
||||
}
|
||||
|
||||
const value = current.panelOpened ?? true
|
||||
if (value === next) return
|
||||
setStore("review", "panelOpened", next)
|
||||
}
|
||||
|
||||
return {
|
||||
scroll(tab: string) {
|
||||
return scroll.scroll(key(), tab)
|
||||
@@ -587,6 +679,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
setTerminalOpened(!terminalOpened())
|
||||
},
|
||||
},
|
||||
reviewPanel: {
|
||||
opened: reviewPanelOpened,
|
||||
open() {
|
||||
setReviewPanelOpened(true)
|
||||
},
|
||||
close() {
|
||||
setReviewPanelOpened(false)
|
||||
},
|
||||
toggle() {
|
||||
setReviewPanelOpened(!reviewPanelOpened())
|
||||
},
|
||||
},
|
||||
review: {
|
||||
open: createMemo(() => s().reviewOpen),
|
||||
setOpen(open: string[]) {
|
||||
@@ -624,11 +728,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
|
||||
return {
|
||||
tabs,
|
||||
active: createMemo(() => (tabs().active === "review" ? undefined : tabs().active)),
|
||||
active: createMemo(() => tabs().active),
|
||||
all: createMemo(() => tabs().all.filter((tab) => tab !== "review")),
|
||||
setActive(tab: string | undefined) {
|
||||
const session = key()
|
||||
if (tab === "review") return
|
||||
if (!store.sessionTabs[session]) {
|
||||
setStore("sessionTabs", session, { all: [], active: tab })
|
||||
} else {
|
||||
@@ -645,10 +748,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
},
|
||||
async open(tab: string) {
|
||||
if (tab === "review") return
|
||||
const session = key()
|
||||
const current = store.sessionTabs[session] ?? { all: [] }
|
||||
|
||||
if (tab === "review") {
|
||||
if (!store.sessionTabs[session]) {
|
||||
setStore("sessionTabs", session, { all: current.all.filter((x) => x !== "review"), active: tab })
|
||||
return
|
||||
}
|
||||
setStore("sessionTabs", session, "active", tab)
|
||||
return
|
||||
}
|
||||
|
||||
if (tab === "context") {
|
||||
const all = [tab, ...current.all.filter((x) => x !== tab)]
|
||||
if (!store.sessionTabs[session]) {
|
||||
@@ -681,13 +792,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
const current = store.sessionTabs[session]
|
||||
if (!current) return
|
||||
|
||||
if (tab === "review") {
|
||||
if (current.active !== tab) return
|
||||
setStore("sessionTabs", session, "active", current.all[0])
|
||||
return
|
||||
}
|
||||
|
||||
const all = current.all.filter((x) => x !== tab)
|
||||
if (current.active !== tab) {
|
||||
setStore("sessionTabs", session, "all", all)
|
||||
return
|
||||
}
|
||||
|
||||
const index = current.all.findIndex((f) => f === tab)
|
||||
const next = current.all[index - 1] ?? current.all[index + 1] ?? all[0]
|
||||
batch(() => {
|
||||
setStore("sessionTabs", session, "all", all)
|
||||
if (current.active !== tab) return
|
||||
|
||||
const index = current.all.findIndex((f) => f === tab)
|
||||
const next = all[index - 1] ?? all[0]
|
||||
setStore("sessionTabs", session, "active", next)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -15,6 +15,9 @@ export type Platform = {
|
||||
/** Open a URL in the default browser */
|
||||
openLink(url: string): void
|
||||
|
||||
/** Open a local path in a local app (desktop only) */
|
||||
openPath?(path: string, app?: string): Promise<void>
|
||||
|
||||
/** Restart the app */
|
||||
restart(): Promise<void>
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createEffect, createMemo, onCleanup, type Accessor } from "solid-js"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { usePlatform } from "./platform"
|
||||
|
||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
name: "SDK",
|
||||
init: (props: { directory: string }) => {
|
||||
init: (props: { directory: Accessor<string> }) => {
|
||||
const platform = usePlatform()
|
||||
const globalSDK = useGlobalSDK()
|
||||
|
||||
const directory = createMemo(() => props.directory)
|
||||
const directory = createMemo(props.directory)
|
||||
const client = createMemo(() =>
|
||||
createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
|
||||
@@ -9,6 +9,8 @@ import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
const keyFor = (directory: string, id: string) => `${directory}\n${id}`
|
||||
|
||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
|
||||
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
name: "Sync",
|
||||
init: () => {
|
||||
@@ -59,7 +61,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const next = items
|
||||
.map((x) => x.info)
|
||||
.filter((m) => !!m?.id)
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
|
||||
batch(() => {
|
||||
input.setStore("message", input.sessionID, reconcile(next, { key: "id" }))
|
||||
@@ -69,7 +71,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
"part",
|
||||
message.info.id,
|
||||
reconcile(
|
||||
message.parts.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id)),
|
||||
message.parts.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
@@ -129,7 +131,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
||||
messages.splice(result.index, 0, message)
|
||||
}
|
||||
draft.part[input.messageID] = input.parts.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id))
|
||||
draft.part[input.messageID] = input.parts.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id))
|
||||
}),
|
||||
)
|
||||
},
|
||||
@@ -271,7 +273,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
await client.session.list().then((x) => {
|
||||
const sessions = (x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
.slice(0, store.limit)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ export type LocalPTY = {
|
||||
cols?: number
|
||||
buffer?: string
|
||||
scrollY?: number
|
||||
tail?: string
|
||||
}
|
||||
|
||||
const WORKSPACE_KEY = "__workspace__"
|
||||
|
||||
@@ -44,7 +44,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "جلسة جديدة",
|
||||
"command.file.open": "فتح ملف",
|
||||
"command.file.open.description": "البحث في الملفات والأوامر",
|
||||
"command.context.addSelection": "إضافة التحديد إلى السياق",
|
||||
"command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي",
|
||||
"command.terminal.toggle": "تبديل المحطة الطرفية",
|
||||
@@ -70,6 +69,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
|
||||
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
|
||||
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
|
||||
"command.workspace.toggle": "تبديل مساحات العمل",
|
||||
"command.session.undo": "تراجع",
|
||||
"command.session.undo.description": "تراجع عن الرسالة الأخيرة",
|
||||
"command.session.redo": "إعادة",
|
||||
@@ -83,7 +83,7 @@ export const dict = {
|
||||
"command.session.unshare": "إلغاء مشاركة الجلسة",
|
||||
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
|
||||
|
||||
"palette.search.placeholder": "البحث في الملفات والأوامر",
|
||||
"palette.search.placeholder": "البحث في الملفات والأوامر والجلسات",
|
||||
"palette.empty": "لا توجد نتائج",
|
||||
"palette.group.commands": "الأوامر",
|
||||
"palette.group.files": "الملفات",
|
||||
@@ -321,22 +321,6 @@ export const dict = {
|
||||
"context.usage.clickToView": "انقر لعرض السياق",
|
||||
"context.usage.view": "عرض استخدام السياق",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "لغة",
|
||||
"toast.language.description": "تم التبديل إلى {{language}}",
|
||||
|
||||
@@ -348,6 +332,11 @@ export const dict = {
|
||||
"toast.permissions.autoaccept.off.title": "توقف قبول التعديلات تلقائيًا",
|
||||
"toast.permissions.autoaccept.off.description": "ستتطلب أذونات التحرير والكتابة موافقة",
|
||||
|
||||
"toast.workspace.enabled.title": "تم تمكين مساحات العمل",
|
||||
"toast.workspace.enabled.description": "الآن يتم عرض عدة worktrees في الشريط الجانبي",
|
||||
"toast.workspace.disabled.title": "تم تعطيل مساحات العمل",
|
||||
"toast.workspace.disabled.description": "يتم عرض worktree الرئيسي فقط في الشريط الجانبي",
|
||||
|
||||
"toast.model.none.title": "لم يتم تحديد نموذج",
|
||||
"toast.model.none.description": "قم بتوصيل موفر لتلخيص هذه الجلسة",
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Nova sessão",
|
||||
"command.file.open": "Abrir arquivo",
|
||||
"command.file.open.description": "Buscar arquivos e comandos",
|
||||
"command.context.addSelection": "Adicionar seleção ao contexto",
|
||||
"command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual",
|
||||
"command.terminal.toggle": "Alternar terminal",
|
||||
@@ -70,6 +69,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Mudar para o próximo nível de esforço",
|
||||
"command.permissions.autoaccept.enable": "Aceitar edições automaticamente",
|
||||
"command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente",
|
||||
"command.workspace.toggle": "Alternar espaços de trabalho",
|
||||
"command.session.undo": "Desfazer",
|
||||
"command.session.undo.description": "Desfazer a última mensagem",
|
||||
"command.session.redo": "Refazer",
|
||||
@@ -83,7 +83,7 @@ export const dict = {
|
||||
"command.session.unshare": "Parar de compartilhar sessão",
|
||||
"command.session.unshare.description": "Parar de compartilhar esta sessão",
|
||||
|
||||
"palette.search.placeholder": "Buscar arquivos e comandos",
|
||||
"palette.search.placeholder": "Buscar arquivos, comandos e sessões",
|
||||
"palette.empty": "Nenhum resultado encontrado",
|
||||
"palette.group.commands": "Comandos",
|
||||
"palette.group.files": "Arquivos",
|
||||
@@ -320,22 +320,6 @@ export const dict = {
|
||||
"context.usage.clickToView": "Clique para ver o contexto",
|
||||
"context.usage.view": "Ver uso do contexto",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Idioma",
|
||||
"toast.language.description": "Alterado para {{language}}",
|
||||
|
||||
@@ -347,6 +331,11 @@ export const dict = {
|
||||
"toast.permissions.autoaccept.off.title": "Parou de aceitar edições automaticamente",
|
||||
"toast.permissions.autoaccept.off.description": "Permissões de edição e escrita exigirão aprovação",
|
||||
|
||||
"toast.workspace.enabled.title": "Espaços de trabalho ativados",
|
||||
"toast.workspace.enabled.description": "Várias worktrees agora são exibidas na barra lateral",
|
||||
"toast.workspace.disabled.title": "Espaços de trabalho desativados",
|
||||
"toast.workspace.disabled.description": "Apenas a worktree principal é exibida na barra lateral",
|
||||
|
||||
"toast.model.none.title": "Nenhum modelo selecionado",
|
||||
"toast.model.none.description": "Conecte um provedor para resumir esta sessão",
|
||||
|
||||
|
||||
@@ -0,0 +1,747 @@
|
||||
export const dict = {
|
||||
"command.category.suggested": "Predloženo",
|
||||
"command.category.view": "Prikaz",
|
||||
"command.category.project": "Projekat",
|
||||
"command.category.provider": "Provajder",
|
||||
"command.category.server": "Server",
|
||||
"command.category.session": "Sesija",
|
||||
"command.category.theme": "Tema",
|
||||
"command.category.language": "Jezik",
|
||||
"command.category.file": "Datoteka",
|
||||
"command.category.context": "Kontekst",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Model",
|
||||
"command.category.mcp": "MCP",
|
||||
"command.category.agent": "Agent",
|
||||
"command.category.permissions": "Dozvole",
|
||||
"command.category.workspace": "Radni prostor",
|
||||
"command.category.settings": "Postavke",
|
||||
|
||||
"theme.scheme.system": "Sistem",
|
||||
"theme.scheme.light": "Svijetlo",
|
||||
"theme.scheme.dark": "Tamno",
|
||||
|
||||
"command.sidebar.toggle": "Prikaži/sakrij bočnu traku",
|
||||
"command.project.open": "Otvori projekat",
|
||||
"command.provider.connect": "Poveži provajdera",
|
||||
"command.server.switch": "Promijeni server",
|
||||
"command.settings.open": "Otvori postavke",
|
||||
"command.session.previous": "Prethodna sesija",
|
||||
"command.session.next": "Sljedeća sesija",
|
||||
"command.session.previous.unseen": "Prethodna nepročitana sesija",
|
||||
"command.session.next.unseen": "Sljedeća nepročitana sesija",
|
||||
"command.session.archive": "Arhiviraj sesiju",
|
||||
|
||||
"command.palette": "Paleta komandi",
|
||||
|
||||
"command.theme.cycle": "Promijeni temu",
|
||||
"command.theme.set": "Koristi temu: {{theme}}",
|
||||
"command.theme.scheme.cycle": "Promijeni šemu boja",
|
||||
"command.theme.scheme.set": "Koristi šemu boja: {{scheme}}",
|
||||
|
||||
"command.language.cycle": "Promijeni jezik",
|
||||
"command.language.set": "Koristi jezik: {{language}}",
|
||||
|
||||
"command.session.new": "Nova sesija",
|
||||
"command.file.open": "Otvori datoteku",
|
||||
"command.tab.close": "Zatvori karticu",
|
||||
"command.context.addSelection": "Dodaj odabir u kontekst",
|
||||
"command.context.addSelection.description": "Dodaj odabrane linije iz trenutne datoteke",
|
||||
"command.terminal.toggle": "Prikaži/sakrij terminal",
|
||||
"command.fileTree.toggle": "Prikaži/sakrij stablo datoteka",
|
||||
"command.review.toggle": "Prikaži/sakrij pregled",
|
||||
"command.terminal.new": "Novi terminal",
|
||||
"command.terminal.new.description": "Kreiraj novu karticu terminala",
|
||||
"command.steps.toggle": "Prikaži/sakrij korake",
|
||||
"command.steps.toggle.description": "Prikaži ili sakrij korake za trenutnu poruku",
|
||||
"command.message.previous": "Prethodna poruka",
|
||||
"command.message.previous.description": "Idi na prethodnu korisničku poruku",
|
||||
"command.message.next": "Sljedeća poruka",
|
||||
"command.message.next.description": "Idi na sljedeću korisničku poruku",
|
||||
"command.model.choose": "Odaberi model",
|
||||
"command.model.choose.description": "Odaberi drugi model",
|
||||
"command.mcp.toggle": "Prikaži/sakrij MCP-ove",
|
||||
"command.mcp.toggle.description": "Prikaži/sakrij MCP-ove",
|
||||
"command.agent.cycle": "Promijeni agenta",
|
||||
"command.agent.cycle.description": "Prebaci na sljedećeg agenta",
|
||||
"command.agent.cycle.reverse": "Promijeni agenta unazad",
|
||||
"command.agent.cycle.reverse.description": "Prebaci na prethodnog agenta",
|
||||
"command.model.variant.cycle": "Promijeni nivo razmišljanja",
|
||||
"command.model.variant.cycle.description": "Prebaci na sljedeći nivo",
|
||||
"command.permissions.autoaccept.enable": "Automatski prihvataj izmjene",
|
||||
"command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje izmjena",
|
||||
"command.workspace.toggle": "Prikaži/sakrij radne prostore",
|
||||
"command.workspace.toggle.description": "Omogući ili onemogući više radnih prostora u bočnoj traci",
|
||||
"command.session.undo": "Poništi",
|
||||
"command.session.undo.description": "Poništi posljednju poruku",
|
||||
"command.session.redo": "Vrati",
|
||||
"command.session.redo.description": "Vrati posljednju poništenu poruku",
|
||||
"command.session.compact": "Sažmi sesiju",
|
||||
"command.session.compact.description": "Sažmi sesiju kako bi se smanjio kontekst",
|
||||
"command.session.fork": "Fork iz poruke",
|
||||
"command.session.fork.description": "Kreiraj novu sesiju iz prethodne poruke",
|
||||
"command.session.share": "Podijeli sesiju",
|
||||
"command.session.share.description": "Podijeli ovu sesiju i kopiraj URL u međuspremnik",
|
||||
"command.session.unshare": "Ukini dijeljenje sesije",
|
||||
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
|
||||
|
||||
"palette.search.placeholder": "Pretraži datoteke, komande i sesije",
|
||||
"palette.empty": "Nema rezultata",
|
||||
"palette.group.commands": "Komande",
|
||||
"palette.group.files": "Datoteke",
|
||||
|
||||
"dialog.provider.search.placeholder": "Pretraži provajdere",
|
||||
"dialog.provider.empty": "Nema pronađenih provajdera",
|
||||
"dialog.provider.group.popular": "Popularno",
|
||||
"dialog.provider.group.other": "Ostalo",
|
||||
"dialog.provider.tag.recommended": "Preporučeno",
|
||||
"dialog.provider.opencode.note": "Kurirani modeli uključujući Claude, GPT, Gemini i druge",
|
||||
"dialog.provider.anthropic.note": "Direktan pristup Claude modelima, uključujući Pro i Max",
|
||||
"dialog.provider.copilot.note": "Claude modeli za pomoć pri kodiranju",
|
||||
"dialog.provider.openai.note": "GPT modeli za brze, sposobne opšte AI zadatke",
|
||||
"dialog.provider.google.note": "Gemini modeli za brze, strukturirane odgovore",
|
||||
"dialog.provider.openrouter.note": "Pristup svim podržanim modelima preko jednog provajdera",
|
||||
"dialog.provider.vercel.note": "Jedinstven pristup AI modelima uz pametno rutiranje",
|
||||
|
||||
"dialog.model.select.title": "Odaberi model",
|
||||
"dialog.model.search.placeholder": "Pretraži modele",
|
||||
"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.unpaid.freeModels.title": "Besplatni modeli koje obezbjeđuje OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Dodaj još modela od popularnih provajdera",
|
||||
|
||||
"dialog.provider.viewAll": "Prikaži više provajdera",
|
||||
|
||||
"provider.connect.title": "Poveži {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Prijavi se putem Claude Pro/Max",
|
||||
"provider.connect.selectMethod": "Odaberi način prijave za {{provider}}.",
|
||||
"provider.connect.method.apiKey": "API ključ",
|
||||
"provider.connect.status.inProgress": "Autorizacija je u toku...",
|
||||
"provider.connect.status.waiting": "Čekanje na autorizaciju...",
|
||||
"provider.connect.status.failed": "Autorizacija nije uspjela: {{error}}",
|
||||
"provider.connect.apiKey.description":
|
||||
"Unesi svoj {{provider}} API ključ da povežeš račun i koristiš {{provider}} modele u OpenCode-u.",
|
||||
"provider.connect.apiKey.label": "{{provider}} API ključ",
|
||||
"provider.connect.apiKey.placeholder": "API ključ",
|
||||
"provider.connect.apiKey.required": "API ključ je obavezan",
|
||||
"provider.connect.opencodeZen.line1":
|
||||
"OpenCode Zen ti daje pristup kuriranom skupu pouzdanih, optimizovanih modela za coding agente.",
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"Sa jednim API ključem dobijaš pristup modelima kao što su Claude, GPT, Gemini, GLM i drugi.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Posjeti ",
|
||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||
"provider.connect.opencodeZen.visit.suffix": " da preuzmeš svoj API ključ.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Posjeti ",
|
||||
"provider.connect.oauth.code.visit.link": "ovaj link",
|
||||
"provider.connect.oauth.code.visit.suffix":
|
||||
" da preuzmeš autorizacijski kod i povežeš račun te koristiš {{provider}} modele u OpenCode-u.",
|
||||
"provider.connect.oauth.code.label": "{{method}} autorizacijski kod",
|
||||
"provider.connect.oauth.code.placeholder": "Autorizacijski kod",
|
||||
"provider.connect.oauth.code.required": "Autorizacijski kod je obavezan",
|
||||
"provider.connect.oauth.code.invalid": "Nevažeći autorizacijski kod",
|
||||
"provider.connect.oauth.auto.visit.prefix": "Posjeti ",
|
||||
"provider.connect.oauth.auto.visit.link": "ovaj link",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" i unesi kod ispod da povežeš račun i koristiš {{provider}} modele u OpenCode-u.",
|
||||
"provider.connect.oauth.auto.confirmationCode": "Kod za potvrdu",
|
||||
"provider.connect.toast.connected.title": "{{provider}} povezan",
|
||||
"provider.connect.toast.connected.description": "{{provider}} modeli su sada dostupni za korištenje.",
|
||||
|
||||
"provider.disconnect.toast.disconnected.title": "{{provider}} odspojen",
|
||||
"provider.disconnect.toast.disconnected.description": "{{provider}} modeli više nisu dostupni.",
|
||||
|
||||
"model.tag.free": "Besplatno",
|
||||
"model.tag.latest": "Najnovije",
|
||||
"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": "slika",
|
||||
"model.input.audio": "zvuk",
|
||||
"model.input.video": "video",
|
||||
"model.input.pdf": "pdf",
|
||||
"model.tooltip.allows": "Podržava: {{inputs}}",
|
||||
"model.tooltip.reasoning.allowed": "Podržava rasuđivanje",
|
||||
"model.tooltip.reasoning.none": "Bez rasuđivanja",
|
||||
"model.tooltip.context": "Limit konteksta {{limit}}",
|
||||
|
||||
"common.search.placeholder": "Pretraži",
|
||||
"common.goBack": "Nazad",
|
||||
"common.goForward": "Naprijed",
|
||||
"common.loading": "Učitavanje",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Otkaži",
|
||||
"common.connect": "Poveži",
|
||||
"common.disconnect": "Prekini vezu",
|
||||
"common.submit": "Pošalji",
|
||||
"common.save": "Sačuvaj",
|
||||
"common.saving": "Čuvanje...",
|
||||
"common.default": "Podrazumijevano",
|
||||
"common.attachment": "prilog",
|
||||
|
||||
"prompt.placeholder.shell": "Unesi shell naredbu...",
|
||||
"prompt.placeholder.normal": 'Pitaj bilo šta... "{{example}}"',
|
||||
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
|
||||
"prompt.placeholder.summarizeComment": "Sažmi komentar…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc za izlaz",
|
||||
|
||||
"prompt.example.1": "Popravi TODO u bazi koda",
|
||||
"prompt.example.2": "Koji je tehnološki stack ovog projekta?",
|
||||
"prompt.example.3": "Popravi pokvarene testove",
|
||||
"prompt.example.4": "Objasni kako radi autentifikacija",
|
||||
"prompt.example.5": "Pronađi i popravi sigurnosne ranjivosti",
|
||||
"prompt.example.6": "Dodaj jedinične testove za servis korisnika",
|
||||
"prompt.example.7": "Refaktoriši ovu funkciju da bude čitljivija",
|
||||
"prompt.example.8": "Šta znači ova greška?",
|
||||
"prompt.example.9": "Pomozi mi da otklonim ovu grešku",
|
||||
"prompt.example.10": "Generiši API dokumentaciju",
|
||||
"prompt.example.11": "Optimizuj upite prema bazi podataka",
|
||||
"prompt.example.12": "Dodaj validaciju ulaza",
|
||||
"prompt.example.13": "Napravi novu komponentu za...",
|
||||
"prompt.example.14": "Kako da deployam ovaj projekat?",
|
||||
"prompt.example.15": "Pregledaj moj kod prema najboljim praksama",
|
||||
"prompt.example.16": "Dodaj obradu grešaka u ovu funkciju",
|
||||
"prompt.example.17": "Objasni ovaj regex obrazac",
|
||||
"prompt.example.18": "Pretvori ovo u TypeScript",
|
||||
"prompt.example.19": "Dodaj logovanje kroz cijelu bazu koda",
|
||||
"prompt.example.20": "Koje su zavisnosti zastarjele?",
|
||||
"prompt.example.21": "Pomozi mi da napišem migracijsku skriptu",
|
||||
"prompt.example.22": "Implementiraj keširanje za ovaj endpoint",
|
||||
"prompt.example.23": "Dodaj paginaciju u ovu listu",
|
||||
"prompt.example.24": "Napravi CLI komandu za...",
|
||||
"prompt.example.25": "Kako ovdje rade varijable okruženja?",
|
||||
|
||||
"prompt.popover.emptyResults": "Nema rezultata",
|
||||
"prompt.popover.emptyCommands": "Nema komandi",
|
||||
"prompt.dropzone.label": "Spusti slike ili PDF-ove ovdje",
|
||||
"prompt.slash.badge.custom": "prilagođeno",
|
||||
"prompt.slash.badge.skill": "skill",
|
||||
"prompt.slash.badge.mcp": "mcp",
|
||||
"prompt.context.active": "aktivno",
|
||||
"prompt.context.includeActiveFile": "Uključi aktivnu datoteku",
|
||||
"prompt.context.removeActiveFile": "Ukloni aktivnu datoteku iz konteksta",
|
||||
"prompt.context.removeFile": "Ukloni datoteku iz konteksta",
|
||||
"prompt.action.attachFile": "Priloži datoteku",
|
||||
"prompt.attachment.remove": "Ukloni prilog",
|
||||
"prompt.action.send": "Pošalji",
|
||||
"prompt.action.stop": "Zaustavi",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Nepodržano lijepljenje",
|
||||
"prompt.toast.pasteUnsupported.description": "Ovdje se mogu zalijepiti samo slike ili PDF-ovi.",
|
||||
"prompt.toast.modelAgentRequired.title": "Odaberi agenta i model",
|
||||
"prompt.toast.modelAgentRequired.description": "Odaberi agenta i model prije slanja upita.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Neuspješno kreiranje worktree-a",
|
||||
"prompt.toast.sessionCreateFailed.title": "Neuspješno kreiranje sesije",
|
||||
"prompt.toast.shellSendFailed.title": "Neuspješno slanje shell naredbe",
|
||||
"prompt.toast.commandSendFailed.title": "Neuspješno slanje komande",
|
||||
"prompt.toast.promptSendFailed.title": "Neuspješno slanje upita",
|
||||
|
||||
"dialog.mcp.title": "MCP-ovi",
|
||||
"dialog.mcp.description": "{{enabled}} od {{total}} omogućeno",
|
||||
"dialog.mcp.empty": "Nema konfigurisnih MCP-ova",
|
||||
|
||||
"dialog.lsp.empty": "LSP-ovi se automatski otkrivaju prema tipu datoteke",
|
||||
"dialog.plugins.empty": "Plugini su konfigurisani u opencode.json",
|
||||
|
||||
"mcp.status.connected": "povezano",
|
||||
"mcp.status.failed": "neuspjelo",
|
||||
"mcp.status.needs_auth": "potrebna autentifikacija",
|
||||
"mcp.status.disabled": "onemogućeno",
|
||||
|
||||
"dialog.fork.empty": "Nema poruka za fork",
|
||||
|
||||
"dialog.directory.search.placeholder": "Pretraži foldere",
|
||||
"dialog.directory.empty": "Nema pronađenih foldera",
|
||||
|
||||
"dialog.server.title": "Serveri",
|
||||
"dialog.server.description": "Promijeni na koji se OpenCode server ova aplikacija povezuje.",
|
||||
"dialog.server.search.placeholder": "Pretraži servere",
|
||||
"dialog.server.empty": "Još nema servera",
|
||||
"dialog.server.add.title": "Dodaj server",
|
||||
"dialog.server.add.url": "URL servera",
|
||||
"dialog.server.add.placeholder": "http://localhost:4096",
|
||||
"dialog.server.add.error": "Nije moguće povezati se na server",
|
||||
"dialog.server.add.checking": "Provjera...",
|
||||
"dialog.server.add.button": "Dodaj server",
|
||||
"dialog.server.default.title": "Podrazumijevani server",
|
||||
"dialog.server.default.description":
|
||||
"Poveži se na ovaj server pri pokretanju aplikacije umjesto pokretanja lokalnog servera. Potreban je restart.",
|
||||
"dialog.server.default.none": "Nije odabran server",
|
||||
"dialog.server.default.set": "Postavi trenutni server kao podrazumijevani",
|
||||
"dialog.server.default.clear": "Očisti",
|
||||
"dialog.server.action.remove": "Ukloni server",
|
||||
|
||||
"dialog.server.menu.edit": "Uredi",
|
||||
"dialog.server.menu.default": "Postavi kao podrazumijevano",
|
||||
"dialog.server.menu.defaultRemove": "Ukloni podrazumijevano",
|
||||
"dialog.server.menu.delete": "Izbriši",
|
||||
"dialog.server.current": "Trenutni server",
|
||||
"dialog.server.status.default": "Podrazumijevano",
|
||||
|
||||
"dialog.project.edit.title": "Uredi projekat",
|
||||
"dialog.project.edit.name": "Naziv",
|
||||
"dialog.project.edit.icon": "Ikonica",
|
||||
"dialog.project.edit.icon.alt": "Ikonica projekta",
|
||||
"dialog.project.edit.icon.hint": "Klikni ili prevuci sliku",
|
||||
"dialog.project.edit.icon.recommended": "Preporučeno: 128x128px",
|
||||
"dialog.project.edit.color": "Boja",
|
||||
"dialog.project.edit.color.select": "Odaberi boju {{color}}",
|
||||
"dialog.project.edit.worktree.startup": "Skripta za pokretanje radnog prostora",
|
||||
"dialog.project.edit.worktree.startup.description": "Pokreće se nakon kreiranja novog radnog prostora (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "npr. bun install",
|
||||
|
||||
"context.breakdown.title": "Razlaganje konteksta",
|
||||
"context.breakdown.note":
|
||||
'Približna raspodjela ulaznih tokena. "Ostalo" uključuje definicije alata i dodatni overhead.',
|
||||
"context.breakdown.system": "Sistem",
|
||||
"context.breakdown.user": "Korisnik",
|
||||
"context.breakdown.assistant": "Asistent",
|
||||
"context.breakdown.tool": "Pozivi alata",
|
||||
"context.breakdown.other": "Ostalo",
|
||||
|
||||
"context.systemPrompt.title": "Sistemski prompt",
|
||||
"context.rawMessages.title": "Sirove poruke",
|
||||
|
||||
"context.stats.session": "Sesija",
|
||||
"context.stats.messages": "Poruke",
|
||||
"context.stats.provider": "Provajder",
|
||||
"context.stats.model": "Model",
|
||||
"context.stats.limit": "Limit konteksta",
|
||||
"context.stats.totalTokens": "Ukupno tokena",
|
||||
"context.stats.usage": "Korištenje",
|
||||
"context.stats.inputTokens": "Ulazni tokeni",
|
||||
"context.stats.outputTokens": "Izlazni tokeni",
|
||||
"context.stats.reasoningTokens": "Tokeni za rasuđivanje",
|
||||
"context.stats.cacheTokens": "Cache tokeni (čitanje/pisanje)",
|
||||
"context.stats.userMessages": "Korisničke poruke",
|
||||
"context.stats.assistantMessages": "Poruke asistenta",
|
||||
"context.stats.totalCost": "Ukupni trošak",
|
||||
"context.stats.sessionCreated": "Sesija kreirana",
|
||||
"context.stats.lastActivity": "Posljednja aktivnost",
|
||||
|
||||
"context.usage.tokens": "Tokeni",
|
||||
"context.usage.usage": "Korištenje",
|
||||
"context.usage.cost": "Trošak",
|
||||
"context.usage.clickToView": "Klikni da vidiš kontekst",
|
||||
"context.usage.view": "Prikaži korištenje konteksta",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Jezik",
|
||||
"toast.language.description": "Prebačeno na {{language}}",
|
||||
|
||||
"toast.theme.title": "Tema promijenjena",
|
||||
"toast.scheme.title": "Šema boja",
|
||||
|
||||
"toast.workspace.enabled.title": "Radni prostori omogućeni",
|
||||
"toast.workspace.enabled.description": "Više worktree-ova se sada prikazuje u bočnoj traci",
|
||||
"toast.workspace.disabled.title": "Radni prostori onemogućeni",
|
||||
"toast.workspace.disabled.description": "Samo glavni worktree se prikazuje u bočnoj traci",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Automatsko prihvatanje izmjena",
|
||||
"toast.permissions.autoaccept.on.description": "Dozvole za izmjene i pisanje biće automatski odobrene",
|
||||
"toast.permissions.autoaccept.off.title": "Zaustavljeno automatsko prihvatanje izmjena",
|
||||
"toast.permissions.autoaccept.off.description": "Dozvole za izmjene i pisanje zahtijevaće odobrenje",
|
||||
|
||||
"toast.model.none.title": "Nije odabran model",
|
||||
"toast.model.none.description": "Poveži provajdera da sažmeš ovu sesiju",
|
||||
|
||||
"toast.file.loadFailed.title": "Neuspjelo učitavanje datoteke",
|
||||
"toast.file.listFailed.title": "Neuspješno listanje datoteka",
|
||||
|
||||
"toast.context.noLineSelection.title": "Nema odabranih linija",
|
||||
"toast.context.noLineSelection.description": "Prvo odaberi raspon linija u kartici datoteke.",
|
||||
|
||||
"toast.session.share.copyFailed.title": "Neuspjelo kopiranje URL-a u međuspremnik",
|
||||
"toast.session.share.success.title": "Sesija podijeljena",
|
||||
"toast.session.share.success.description": "URL za dijeljenje je kopiran u međuspremnik!",
|
||||
"toast.session.share.failed.title": "Neuspjelo dijeljenje sesije",
|
||||
"toast.session.share.failed.description": "Došlo je do greške prilikom dijeljenja sesije",
|
||||
|
||||
"toast.session.unshare.success.title": "Dijeljenje sesije ukinuto",
|
||||
"toast.session.unshare.success.description": "Dijeljenje sesije je uspješno ukinuto!",
|
||||
"toast.session.unshare.failed.title": "Neuspjelo ukidanje dijeljenja",
|
||||
"toast.session.unshare.failed.description": "Došlo je do greške prilikom ukidanja dijeljenja",
|
||||
|
||||
"toast.session.listFailed.title": "Neuspjelo učitavanje sesija za {{project}}",
|
||||
|
||||
"toast.update.title": "Dostupno ažuriranje",
|
||||
"toast.update.description": "Nova verzija OpenCode-a ({{version}}) je dostupna za instalaciju.",
|
||||
"toast.update.action.installRestart": "Instaliraj i restartuj",
|
||||
"toast.update.action.notYet": "Ne još",
|
||||
|
||||
"error.page.title": "Nešto je pošlo po zlu",
|
||||
"error.page.description": "Došlo je do greške prilikom učitavanja aplikacije.",
|
||||
"error.page.details.label": "Detalji greške",
|
||||
"error.page.action.restart": "Restartuj",
|
||||
"error.page.action.checking": "Provjera...",
|
||||
"error.page.action.checkUpdates": "Provjeri ažuriranja",
|
||||
"error.page.action.updateTo": "Ažuriraj na {{version}}",
|
||||
"error.page.report.prefix": "Molimo prijavi ovu grešku OpenCode timu",
|
||||
"error.page.report.discord": "na Discordu",
|
||||
"error.page.version": "Verzija: {{version}}",
|
||||
|
||||
"error.dev.rootNotFound":
|
||||
"Korijenski element nije pronađen. Da li si zaboravio da ga dodaš u index.html? Ili je možda id atribut pogrešno napisan?",
|
||||
|
||||
"error.globalSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?",
|
||||
|
||||
"error.chain.unknown": "Nepoznata greška",
|
||||
"error.chain.causedBy": "Uzrok:",
|
||||
"error.chain.apiError": "API greška",
|
||||
"error.chain.status": "Status: {{status}}",
|
||||
"error.chain.retryable": "Može se ponoviti: {{retryable}}",
|
||||
"error.chain.responseBody": "Tijelo odgovora:\n{{body}}",
|
||||
"error.chain.didYouMean": "Da li si mislio: {{suggestions}}",
|
||||
"error.chain.modelNotFound": "Model nije pronađen: {{provider}}/{{model}}",
|
||||
"error.chain.checkConfig": "Provjeri konfiguraciju (opencode.json) - nazive provajdera/modela",
|
||||
"error.chain.mcpFailed": 'MCP server "{{name}}" nije uspio. Napomena: OpenCode još ne podržava MCP autentifikaciju.',
|
||||
"error.chain.providerAuthFailed": "Autentifikacija provajdera nije uspjela ({{provider}}): {{message}}",
|
||||
"error.chain.providerInitFailed":
|
||||
'Neuspjelo inicijalizovanje provajdera "{{provider}}". Provjeri kredencijale i konfiguraciju.',
|
||||
"error.chain.configJsonInvalid": "Konfiguracijska datoteka na {{path}} nije važeći JSON(C)",
|
||||
"error.chain.configJsonInvalidWithMessage": "Konfiguracijska datoteka na {{path}} nije važeći JSON(C): {{message}}",
|
||||
"error.chain.configDirectoryTypo":
|
||||
'Direktorij "{{dir}}" u {{path}} nije ispravan. Preimenuj direktorij u "{{suggestion}}" ili ga ukloni. Ovo je česta greška u kucanju.',
|
||||
"error.chain.configFrontmatterError": "Neuspjelo parsiranje frontmatter-a u {{path}}:\n{{message}}",
|
||||
"error.chain.configInvalid": "Konfiguracijska datoteka na {{path}} nije ispravna",
|
||||
"error.chain.configInvalidWithMessage": "Konfiguracijska datoteka na {{path}} nije ispravna: {{message}}",
|
||||
|
||||
"notification.permission.title": "Potrebna dozvola",
|
||||
"notification.permission.description": "{{sessionTitle}} u {{projectName}} traži dozvolu",
|
||||
"notification.question.title": "Pitanje",
|
||||
"notification.question.description": "{{sessionTitle}} u {{projectName}} ima pitanje",
|
||||
"notification.action.goToSession": "Idi na sesiju",
|
||||
|
||||
"notification.session.responseReady.title": "Odgovor je spreman",
|
||||
"notification.session.error.title": "Greška sesije",
|
||||
"notification.session.error.fallbackDescription": "Došlo je do greške",
|
||||
|
||||
"home.recentProjects": "Nedavni projekti",
|
||||
"home.empty.title": "Nema nedavnih projekata",
|
||||
"home.empty.description": "Kreni tako što ćeš otvoriti lokalni projekat",
|
||||
|
||||
"session.tab.session": "Sesija",
|
||||
"session.tab.review": "Pregled",
|
||||
"session.tab.context": "Kontekst",
|
||||
"session.panel.reviewAndFiles": "Pregled i datoteke",
|
||||
"session.review.filesChanged": "Izmijenjeno {{count}} datoteka",
|
||||
"session.review.change.one": "Izmjena",
|
||||
"session.review.change.other": "Izmjene",
|
||||
"session.review.loadingChanges": "Učitavanje izmjena...",
|
||||
"session.review.empty": "Još nema izmjena u ovoj sesiji",
|
||||
"session.review.noChanges": "Nema izmjena",
|
||||
|
||||
"session.files.selectToOpen": "Odaberi datoteku za otvaranje",
|
||||
"session.files.all": "Sve datoteke",
|
||||
"session.files.binaryContent": "Binarna datoteka (sadržaj se ne može prikazati)",
|
||||
|
||||
"session.messages.renderEarlier": "Prikaži ranije poruke",
|
||||
"session.messages.loadingEarlier": "Učitavanje ranijih poruka...",
|
||||
"session.messages.loadEarlier": "Učitaj ranije poruke",
|
||||
"session.messages.loading": "Učitavanje poruka...",
|
||||
"session.messages.jumpToLatest": "Idi na najnovije",
|
||||
|
||||
"session.context.addToContext": "Dodaj {{selection}} u kontekst",
|
||||
|
||||
"session.new.worktree.main": "Glavna grana",
|
||||
"session.new.worktree.mainWithBranch": "Glavna grana ({{branch}})",
|
||||
"session.new.worktree.create": "Kreiraj novi worktree",
|
||||
"session.new.lastModified": "Posljednja izmjena",
|
||||
|
||||
"session.header.search.placeholder": "Pretraži {{project}}",
|
||||
"session.header.searchFiles": "Pretraži datoteke",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Konfiguracije servera",
|
||||
"status.popover.tab.servers": "Serveri",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugini",
|
||||
"status.popover.action.manageServers": "Upravljaj serverima",
|
||||
|
||||
"session.share.popover.title": "Objavi na webu",
|
||||
"session.share.popover.description.shared": "Ova sesija je javna na webu. Dostupna je svima koji imaju link.",
|
||||
"session.share.popover.description.unshared": "Podijeli sesiju javno na webu. Biće dostupna svima koji imaju link.",
|
||||
"session.share.action.share": "Podijeli",
|
||||
"session.share.action.publish": "Objavi",
|
||||
"session.share.action.publishing": "Objavljivanje...",
|
||||
"session.share.action.unpublish": "Poništi objavu",
|
||||
"session.share.action.unpublishing": "Poništavanje objave...",
|
||||
"session.share.action.view": "Prikaži",
|
||||
"session.share.copy.copied": "Kopirano",
|
||||
"session.share.copy.copyLink": "Kopiraj link",
|
||||
|
||||
"lsp.tooltip.none": "Nema LSP servera",
|
||||
"lsp.label.connected": "{{count}} LSP",
|
||||
|
||||
"prompt.loading": "Učitavanje upita...",
|
||||
"terminal.loading": "Učitavanje terminala...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.close": "Zatvori terminal",
|
||||
"terminal.connectionLost.title": "Veza prekinuta",
|
||||
"terminal.connectionLost.description":
|
||||
"Veza s terminalom je prekinuta. Ovo se može desiti kada se server restartuje.",
|
||||
|
||||
"common.closeTab": "Zatvori karticu",
|
||||
"common.dismiss": "Odbaci",
|
||||
"common.requestFailed": "Zahtjev nije uspio",
|
||||
"common.moreOptions": "Više opcija",
|
||||
"common.learnMore": "Saznaj više",
|
||||
"common.rename": "Preimenuj",
|
||||
"common.reset": "Resetuj",
|
||||
"common.archive": "Arhiviraj",
|
||||
"common.delete": "Izbriši",
|
||||
"common.close": "Zatvori",
|
||||
"common.edit": "Uredi",
|
||||
"common.loadMore": "Učitaj još",
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "Prikaži/sakrij meni",
|
||||
"sidebar.nav.projectsAndSessions": "Projekti i sesije",
|
||||
"sidebar.settings": "Postavke",
|
||||
"sidebar.help": "Pomoć",
|
||||
"sidebar.workspaces.enable": "Omogući radne prostore",
|
||||
"sidebar.workspaces.disable": "Onemogući radne prostore",
|
||||
"sidebar.gettingStarted.title": "Početak",
|
||||
"sidebar.gettingStarted.line1": "OpenCode uključuje besplatne modele, tako da možeš odmah početi.",
|
||||
"sidebar.gettingStarted.line2": "Poveži bilo kojeg provajdera da koristiš modele, npr. Claude, GPT, Gemini itd.",
|
||||
"sidebar.project.recentSessions": "Nedavne sesije",
|
||||
"sidebar.project.viewAllSessions": "Prikaži sve sesije",
|
||||
|
||||
"app.name.desktop": "OpenCode Desktop",
|
||||
|
||||
"settings.section.desktop": "Desktop",
|
||||
"settings.section.server": "Server",
|
||||
"settings.tab.general": "Opšte",
|
||||
"settings.tab.shortcuts": "Prečice",
|
||||
|
||||
"settings.general.section.appearance": "Izgled",
|
||||
"settings.general.section.notifications": "Sistemske obavijesti",
|
||||
"settings.general.section.updates": "Ažuriranja",
|
||||
"settings.general.section.sounds": "Zvučni efekti",
|
||||
|
||||
"settings.general.row.language.title": "Jezik",
|
||||
"settings.general.row.language.description": "Promijeni jezik prikaza u OpenCode-u",
|
||||
"settings.general.row.appearance.title": "Izgled",
|
||||
"settings.general.row.appearance.description": "Prilagodi kako OpenCode izgleda na tvom uređaju",
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Prilagodi temu OpenCode-a.",
|
||||
"settings.general.row.font.title": "Font",
|
||||
"settings.general.row.font.description": "Prilagodi monospace font koji se koristi u blokovima koda",
|
||||
|
||||
"settings.general.row.releaseNotes.title": "Bilješke o izdanju",
|
||||
"settings.general.row.releaseNotes.description": 'Prikaži iskačuće prozore "Šta je novo" nakon ažuriranja',
|
||||
|
||||
"settings.updates.row.startup.title": "Provjeri ažuriranja pri pokretanju",
|
||||
"settings.updates.row.startup.description": "Automatski provjerava ažuriranja kada se OpenCode pokrene",
|
||||
"settings.updates.row.check.title": "Provjeri ažuriranja",
|
||||
"settings.updates.row.check.description": "Ručno provjeri ažuriranja i instaliraj ako su dostupna",
|
||||
"settings.updates.action.checkNow": "Provjeri sada",
|
||||
"settings.updates.action.checking": "Provjera...",
|
||||
"settings.updates.toast.latest.title": "Sve je ažurno",
|
||||
"settings.updates.toast.latest.description": "Koristiš najnoviju verziju OpenCode-a.",
|
||||
"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",
|
||||
"sound.option.alert01": "Upozorenje 01",
|
||||
"sound.option.alert02": "Upozorenje 02",
|
||||
"sound.option.alert03": "Upozorenje 03",
|
||||
"sound.option.alert04": "Upozorenje 04",
|
||||
"sound.option.alert05": "Upozorenje 05",
|
||||
"sound.option.alert06": "Upozorenje 06",
|
||||
"sound.option.alert07": "Upozorenje 07",
|
||||
"sound.option.alert08": "Upozorenje 08",
|
||||
"sound.option.alert09": "Upozorenje 09",
|
||||
"sound.option.alert10": "Upozorenje 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": "Ne 01",
|
||||
"sound.option.nope02": "Ne 02",
|
||||
"sound.option.nope03": "Ne 03",
|
||||
"sound.option.nope04": "Ne 04",
|
||||
"sound.option.nope05": "Ne 05",
|
||||
"sound.option.nope06": "Ne 06",
|
||||
"sound.option.nope07": "Ne 07",
|
||||
"sound.option.nope08": "Ne 08",
|
||||
"sound.option.nope09": "Ne 09",
|
||||
"sound.option.nope10": "Ne 10",
|
||||
"sound.option.nope11": "Ne 11",
|
||||
"sound.option.nope12": "Ne 12",
|
||||
"sound.option.yup01": "Da 01",
|
||||
"sound.option.yup02": "Da 02",
|
||||
"sound.option.yup03": "Da 03",
|
||||
"sound.option.yup04": "Da 04",
|
||||
"sound.option.yup05": "Da 05",
|
||||
"sound.option.yup06": "Da 06",
|
||||
|
||||
"settings.general.notifications.agent.title": "Agent",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Prikaži sistemsku obavijest kada agent završi ili zahtijeva pažnju",
|
||||
"settings.general.notifications.permissions.title": "Dozvole",
|
||||
"settings.general.notifications.permissions.description": "Prikaži sistemsku obavijest kada je potrebna dozvola",
|
||||
"settings.general.notifications.errors.title": "Greške",
|
||||
"settings.general.notifications.errors.description": "Prikaži sistemsku obavijest kada dođe do greške",
|
||||
|
||||
"settings.general.sounds.agent.title": "Agent",
|
||||
"settings.general.sounds.agent.description": "Pusti zvuk kada agent završi ili zahtijeva pažnju",
|
||||
"settings.general.sounds.permissions.title": "Dozvole",
|
||||
"settings.general.sounds.permissions.description": "Pusti zvuk kada je potrebna dozvola",
|
||||
"settings.general.sounds.errors.title": "Greške",
|
||||
"settings.general.sounds.errors.description": "Pusti zvuk kada dođe do greške",
|
||||
|
||||
"settings.shortcuts.title": "Prečice na tastaturi",
|
||||
"settings.shortcuts.reset.button": "Vrati na podrazumijevano",
|
||||
"settings.shortcuts.reset.toast.title": "Prečice resetovane",
|
||||
"settings.shortcuts.reset.toast.description": "Prečice na tastaturi su vraćene na podrazumijevane.",
|
||||
"settings.shortcuts.conflict.title": "Prečica je već u upotrebi",
|
||||
"settings.shortcuts.conflict.description": "{{keybind}} je već dodijeljeno za {{titles}}.",
|
||||
"settings.shortcuts.unassigned": "Nedodijeljeno",
|
||||
"settings.shortcuts.pressKeys": "Pritisni tastere",
|
||||
"settings.shortcuts.search.placeholder": "Pretraži prečice",
|
||||
"settings.shortcuts.search.empty": "Nema pronađenih prečica",
|
||||
|
||||
"settings.shortcuts.group.general": "Opšte",
|
||||
"settings.shortcuts.group.session": "Sesija",
|
||||
"settings.shortcuts.group.navigation": "Navigacija",
|
||||
"settings.shortcuts.group.modelAndAgent": "Model i agent",
|
||||
"settings.shortcuts.group.terminal": "Terminal",
|
||||
"settings.shortcuts.group.prompt": "Upit",
|
||||
|
||||
"settings.providers.title": "Provajderi",
|
||||
"settings.providers.description": "Postavke provajdera će se ovdje moći podešavati.",
|
||||
"settings.providers.section.connected": "Povezani provajderi",
|
||||
"settings.providers.connected.empty": "Nema povezanih provajdera",
|
||||
"settings.providers.section.popular": "Popularni provajderi",
|
||||
"settings.providers.tag.environment": "Okruženje",
|
||||
"settings.providers.tag.config": "Konfiguracija",
|
||||
"settings.providers.tag.custom": "Prilagođeno",
|
||||
"settings.providers.tag.other": "Ostalo",
|
||||
"settings.models.title": "Modeli",
|
||||
"settings.models.description": "Postavke modela će se ovdje moći podešavati.",
|
||||
"settings.agents.title": "Agenti",
|
||||
"settings.agents.description": "Postavke agenata će se ovdje moći podešavati.",
|
||||
"settings.commands.title": "Komande",
|
||||
"settings.commands.description": "Postavke komandi će se ovdje moći podešavati.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP postavke će se ovdje moći podešavati.",
|
||||
|
||||
"settings.permissions.title": "Dozvole",
|
||||
"settings.permissions.description": "Kontroliši koje alate server smije koristiti po defaultu.",
|
||||
"settings.permissions.section.tools": "Alati",
|
||||
"settings.permissions.toast.updateFailed.title": "Neuspjelo ažuriranje dozvola",
|
||||
|
||||
"settings.permissions.action.allow": "Dozvoli",
|
||||
"settings.permissions.action.ask": "Pitaj",
|
||||
"settings.permissions.action.deny": "Zabrani",
|
||||
|
||||
"settings.permissions.tool.read.title": "Čitanje",
|
||||
"settings.permissions.tool.read.description": "Čitanje datoteke (podudara se s putanjom datoteke)",
|
||||
"settings.permissions.tool.edit.title": "Uređivanje",
|
||||
"settings.permissions.tool.edit.description":
|
||||
"Mijenjanje datoteka, uključujući izmjene, pisanja, patch-eve i multi-izmjene",
|
||||
"settings.permissions.tool.glob.title": "Glob",
|
||||
"settings.permissions.tool.glob.description": "Podudaranje datoteka pomoću glob šablona",
|
||||
"settings.permissions.tool.grep.title": "Grep",
|
||||
"settings.permissions.tool.grep.description": "Pretraživanje sadržaja datoteka pomoću regularnih izraza",
|
||||
"settings.permissions.tool.list.title": "Lista",
|
||||
"settings.permissions.tool.list.description": "Listanje datoteka unutar direktorija",
|
||||
"settings.permissions.tool.bash.title": "Bash",
|
||||
"settings.permissions.tool.bash.description": "Pokretanje shell komandi",
|
||||
"settings.permissions.tool.task.title": "Zadatak",
|
||||
"settings.permissions.tool.task.description": "Pokretanje pod-agenta",
|
||||
"settings.permissions.tool.skill.title": "Vještina",
|
||||
"settings.permissions.tool.skill.description": "Učitaj vještinu po nazivu",
|
||||
"settings.permissions.tool.lsp.title": "LSP",
|
||||
"settings.permissions.tool.lsp.description": "Pokreni upite jezičnog servera",
|
||||
"settings.permissions.tool.todoread.title": "Čitanje liste zadataka",
|
||||
"settings.permissions.tool.todoread.description": "Čitanje liste zadataka",
|
||||
"settings.permissions.tool.todowrite.title": "Ažuriranje liste zadataka",
|
||||
"settings.permissions.tool.todowrite.description": "Ažuriraj listu zadataka",
|
||||
"settings.permissions.tool.webfetch.title": "Web preuzimanje",
|
||||
"settings.permissions.tool.webfetch.description": "Preuzmi sadržaj sa URL-a",
|
||||
"settings.permissions.tool.websearch.title": "Web pretraga",
|
||||
"settings.permissions.tool.websearch.description": "Pretražuj web",
|
||||
"settings.permissions.tool.codesearch.title": "Pretraga koda",
|
||||
"settings.permissions.tool.codesearch.description": "Pretraži kod na webu",
|
||||
"settings.permissions.tool.external_directory.title": "Vanjski direktorij",
|
||||
"settings.permissions.tool.external_directory.description": "Pristup datotekama izvan direktorija projekta",
|
||||
"settings.permissions.tool.doom_loop.title": "Beskonačna petlja",
|
||||
"settings.permissions.tool.doom_loop.description": "Otkriva ponovljene pozive alata sa identičnim unosom",
|
||||
|
||||
"session.delete.failed.title": "Neuspjelo brisanje sesije",
|
||||
"session.delete.title": "Izbriši sesiju",
|
||||
"session.delete.confirm": 'Izbriši sesiju "{{name}}"?',
|
||||
"session.delete.button": "Izbriši sesiju",
|
||||
|
||||
"workspace.new": "Novi radni prostor",
|
||||
"workspace.type.local": "lokalno",
|
||||
"workspace.type.sandbox": "sandbox",
|
||||
"workspace.create.failed.title": "Neuspješno kreiranje radnog prostora",
|
||||
"workspace.delete.failed.title": "Neuspješno brisanje radnog prostora",
|
||||
"workspace.resetting.title": "Resetovanje radnog prostora",
|
||||
"workspace.resetting.description": "Ovo može potrajati minut.",
|
||||
"workspace.reset.failed.title": "Neuspješno resetovanje radnog prostora",
|
||||
"workspace.reset.success.title": "Radni prostor resetovan",
|
||||
"workspace.reset.success.description": "Radni prostor sada odgovara podrazumijevanoj grani.",
|
||||
"workspace.error.stillPreparing": "Radni prostor se još priprema",
|
||||
"workspace.status.checking": "Provjera neobjedinjenih promjena...",
|
||||
"workspace.status.error": "Nije moguće provjeriti git status.",
|
||||
"workspace.status.clean": "Nisu pronađene neobjedinjene promjene.",
|
||||
"workspace.status.dirty": "Pronađene su neobjedinjene promjene u ovom radnom prostoru.",
|
||||
"workspace.delete.title": "Izbriši radni prostor",
|
||||
"workspace.delete.confirm": 'Izbriši radni prostor "{{name}}"?',
|
||||
"workspace.delete.button": "Izbriši radni prostor",
|
||||
"workspace.reset.title": "Resetuj radni prostor",
|
||||
"workspace.reset.confirm": 'Resetuj radni prostor "{{name}}"?',
|
||||
"workspace.reset.button": "Resetuj radni prostor",
|
||||
"workspace.reset.archived.none": "Nijedna aktivna sesija neće biti arhivirana.",
|
||||
"workspace.reset.archived.one": "1 sesija će biti arhivirana.",
|
||||
"workspace.reset.archived.many": "Biće arhivirano {{count}} sesija.",
|
||||
"workspace.reset.note": "Ovo će resetovati radni prostor da odgovara podrazumijevanoj grani.",
|
||||
}
|
||||
@@ -44,7 +44,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Ny session",
|
||||
"command.file.open": "Åbn fil",
|
||||
"command.file.open.description": "Søg i filer og kommandoer",
|
||||
"command.context.addSelection": "Tilføj markering til kontekst",
|
||||
"command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil",
|
||||
"command.terminal.toggle": "Skift terminal",
|
||||
@@ -70,6 +69,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Skift til næste indsatsniveau",
|
||||
"command.permissions.autoaccept.enable": "Accepter ændringer automatisk",
|
||||
"command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer",
|
||||
"command.workspace.toggle": "Skift arbejdsområder",
|
||||
"command.session.undo": "Fortryd",
|
||||
"command.session.undo.description": "Fortryd den sidste besked",
|
||||
"command.session.redo": "Omgør",
|
||||
@@ -83,7 +83,7 @@ export const dict = {
|
||||
"command.session.unshare": "Stop deling af session",
|
||||
"command.session.unshare.description": "Stop med at dele denne session",
|
||||
|
||||
"palette.search.placeholder": "Søg i filer og kommandoer",
|
||||
"palette.search.placeholder": "Søg i filer, kommandoer og sessioner",
|
||||
"palette.empty": "Ingen resultater fundet",
|
||||
"palette.group.commands": "Kommandoer",
|
||||
"palette.group.files": "Filer",
|
||||
@@ -322,22 +322,6 @@ export const dict = {
|
||||
"context.usage.clickToView": "Klik for at se kontekst",
|
||||
"context.usage.view": "Se kontekstforbrug",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Sprog",
|
||||
"toast.language.description": "Skiftede til {{language}}",
|
||||
|
||||
@@ -349,6 +333,11 @@ export const dict = {
|
||||
"toast.permissions.autoaccept.off.title": "Stoppede automatisk accept af ændringer",
|
||||
"toast.permissions.autoaccept.off.description": "Redigerings- og skrivetilladelser vil kræve godkendelse",
|
||||
|
||||
"toast.workspace.enabled.title": "Arbejdsområder aktiveret",
|
||||
"toast.workspace.enabled.description": "Flere worktrees vises nu i sidepanelet",
|
||||
"toast.workspace.disabled.title": "Arbejdsområder deaktiveret",
|
||||
"toast.workspace.disabled.description": "Kun hoved-worktree vises i sidepanelet",
|
||||
|
||||
"toast.model.none.title": "Ingen model valgt",
|
||||
"toast.model.none.description": "Forbind en udbyder for at opsummere denne session",
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Neue Sitzung",
|
||||
"command.file.open": "Datei öffnen",
|
||||
"command.file.open.description": "Dateien und Befehle durchsuchen",
|
||||
"command.context.addSelection": "Auswahl zum Kontext hinzufügen",
|
||||
"command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen",
|
||||
"command.terminal.toggle": "Terminal umschalten",
|
||||
@@ -74,6 +73,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln",
|
||||
"command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren",
|
||||
"command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen",
|
||||
"command.workspace.toggle": "Arbeitsbereiche umschalten",
|
||||
"command.session.undo": "Rückgängig",
|
||||
"command.session.undo.description": "Letzte Nachricht rückgängig machen",
|
||||
"command.session.redo": "Wiederherstellen",
|
||||
@@ -87,7 +87,7 @@ export const dict = {
|
||||
"command.session.unshare": "Teilen der Sitzung aufheben",
|
||||
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
|
||||
|
||||
"palette.search.placeholder": "Dateien und Befehle durchsuchen",
|
||||
"palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen",
|
||||
"palette.empty": "Keine Ergebnisse gefunden",
|
||||
"palette.group.commands": "Befehle",
|
||||
"palette.group.files": "Dateien",
|
||||
@@ -328,22 +328,6 @@ export const dict = {
|
||||
"context.usage.clickToView": "Klicken, um Kontext anzuzeigen",
|
||||
"context.usage.view": "Kontextnutzung anzeigen",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Sprache",
|
||||
"toast.language.description": "Zu {{language}} gewechselt",
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "New session",
|
||||
"command.file.open": "Open file",
|
||||
"command.file.open.description": "Search files and commands",
|
||||
"command.tab.close": "Close tab",
|
||||
"command.context.addSelection": "Add selection to context",
|
||||
"command.context.addSelection.description": "Add selected lines from the current file",
|
||||
@@ -71,6 +70,8 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Switch to the next effort level",
|
||||
"command.permissions.autoaccept.enable": "Auto-accept edits",
|
||||
"command.permissions.autoaccept.disable": "Stop auto-accepting edits",
|
||||
"command.workspace.toggle": "Toggle workspaces",
|
||||
"command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar",
|
||||
"command.session.undo": "Undo",
|
||||
"command.session.undo.description": "Undo the last message",
|
||||
"command.session.redo": "Redo",
|
||||
@@ -84,7 +85,7 @@ export const dict = {
|
||||
"command.session.unshare": "Unshare session",
|
||||
"command.session.unshare.description": "Stop sharing this session",
|
||||
|
||||
"palette.search.placeholder": "Search files and commands",
|
||||
"palette.search.placeholder": "Search files, commands, and sessions",
|
||||
"palette.empty": "No results found",
|
||||
"palette.group.commands": "Commands",
|
||||
"palette.group.files": "Files",
|
||||
@@ -342,6 +343,7 @@ export const dict = {
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.bs": "Bosanski",
|
||||
"language.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Language",
|
||||
@@ -350,6 +352,11 @@ export const dict = {
|
||||
"toast.theme.title": "Theme switched",
|
||||
"toast.scheme.title": "Color scheme",
|
||||
|
||||
"toast.workspace.enabled.title": "Workspaces enabled",
|
||||
"toast.workspace.enabled.description": "Multiple worktrees are now shown in the sidebar",
|
||||
"toast.workspace.disabled.title": "Workspaces disabled",
|
||||
"toast.workspace.disabled.description": "Only the main worktree is shown in the sidebar",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Auto-accepting edits",
|
||||
"toast.permissions.autoaccept.on.description": "Edit and write permissions will be automatically approved",
|
||||
"toast.permissions.autoaccept.off.title": "Stopped auto-accepting edits",
|
||||
@@ -463,6 +470,11 @@ export const dict = {
|
||||
|
||||
"session.header.search.placeholder": "Search {{project}}",
|
||||
"session.header.searchFiles": "Search files",
|
||||
"session.header.openIn": "Open in",
|
||||
"session.header.open.action": "Open {{app}}",
|
||||
"session.header.open.ariaLabel": "Open in {{app}}",
|
||||
"session.header.open.menu": "Open options",
|
||||
"session.header.open.copyPath": "Copy Path",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Server configurations",
|
||||
|
||||
@@ -44,7 +44,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Nueva sesión",
|
||||
"command.file.open": "Abrir archivo",
|
||||
"command.file.open.description": "Buscar archivos y comandos",
|
||||
"command.context.addSelection": "Añadir selección al contexto",
|
||||
"command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual",
|
||||
"command.terminal.toggle": "Alternar terminal",
|
||||
@@ -70,6 +69,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo",
|
||||
"command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente",
|
||||
"command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente",
|
||||
"command.workspace.toggle": "Alternar espacios de trabajo",
|
||||
"command.session.undo": "Deshacer",
|
||||
"command.session.undo.description": "Deshacer el último mensaje",
|
||||
"command.session.redo": "Rehacer",
|
||||
@@ -83,7 +83,7 @@ export const dict = {
|
||||
"command.session.unshare": "Dejar de compartir sesión",
|
||||
"command.session.unshare.description": "Dejar de compartir esta sesión",
|
||||
|
||||
"palette.search.placeholder": "Buscar archivos y comandos",
|
||||
"palette.search.placeholder": "Buscar archivos, comandos y sesiones",
|
||||
"palette.empty": "No se encontraron resultados",
|
||||
"palette.group.commands": "Comandos",
|
||||
"palette.group.files": "Archivos",
|
||||
@@ -323,22 +323,6 @@ export const dict = {
|
||||
"context.usage.clickToView": "Haz clic para ver contexto",
|
||||
"context.usage.view": "Ver uso del contexto",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Idioma",
|
||||
"toast.language.description": "Cambiado a {{language}}",
|
||||
|
||||
@@ -350,6 +334,11 @@ export const dict = {
|
||||
"toast.permissions.autoaccept.off.title": "Se dejó de aceptar ediciones automáticamente",
|
||||
"toast.permissions.autoaccept.off.description": "Los permisos de edición y escritura requerirán aprobación",
|
||||
|
||||
"toast.workspace.enabled.title": "Espacios de trabajo habilitados",
|
||||
"toast.workspace.enabled.description": "Ahora se muestran varios worktrees en la barra lateral",
|
||||
"toast.workspace.disabled.title": "Espacios de trabajo deshabilitados",
|
||||
"toast.workspace.disabled.description": "Solo se muestra el worktree principal en la barra lateral",
|
||||
|
||||
"toast.model.none.title": "Ningún modelo seleccionado",
|
||||
"toast.model.none.description": "Conecta un proveedor para resumir esta sesión",
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Nouvelle session",
|
||||
"command.file.open": "Ouvrir un fichier",
|
||||
"command.file.open.description": "Rechercher des fichiers et des commandes",
|
||||
"command.context.addSelection": "Ajouter la sélection au contexte",
|
||||
"command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel",
|
||||
"command.terminal.toggle": "Basculer le terminal",
|
||||
@@ -70,6 +69,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Passer au niveau d'effort suivant",
|
||||
"command.permissions.autoaccept.enable": "Accepter automatiquement les modifications",
|
||||
"command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications",
|
||||
"command.workspace.toggle": "Basculer les espaces de travail",
|
||||
"command.session.undo": "Annuler",
|
||||
"command.session.undo.description": "Annuler le dernier message",
|
||||
"command.session.redo": "Rétablir",
|
||||
@@ -83,7 +83,7 @@ export const dict = {
|
||||
"command.session.unshare": "Ne plus partager la session",
|
||||
"command.session.unshare.description": "Arrêter de partager cette session",
|
||||
|
||||
"palette.search.placeholder": "Rechercher des fichiers et des commandes",
|
||||
"palette.search.placeholder": "Rechercher des fichiers, des commandes et des sessions",
|
||||
"palette.empty": "Aucun résultat trouvé",
|
||||
"palette.group.commands": "Commandes",
|
||||
"palette.group.files": "Fichiers",
|
||||
@@ -323,22 +323,6 @@ export const dict = {
|
||||
"context.usage.clickToView": "Cliquez pour voir le contexte",
|
||||
"context.usage.view": "Voir l'utilisation du contexte",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Langue",
|
||||
"toast.language.description": "Passé à {{language}}",
|
||||
|
||||
@@ -352,6 +336,11 @@ export const dict = {
|
||||
"toast.permissions.autoaccept.off.description":
|
||||
"Les permissions de modification et d'écriture nécessiteront une approbation",
|
||||
|
||||
"toast.workspace.enabled.title": "Espaces de travail activés",
|
||||
"toast.workspace.enabled.description": "Plusieurs worktrees sont désormais affichés dans la barre latérale",
|
||||
"toast.workspace.disabled.title": "Espaces de travail désactivés",
|
||||
"toast.workspace.disabled.description": "Seul le worktree principal est affiché dans la barre latérale",
|
||||
|
||||
"toast.model.none.title": "Aucun modèle sélectionné",
|
||||
"toast.model.none.description": "Connectez un fournisseur pour résumer cette session",
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "新しいセッション",
|
||||
"command.file.open": "ファイルを開く",
|
||||
"command.file.open.description": "ファイルとコマンドを検索",
|
||||
"command.context.addSelection": "選択範囲をコンテキストに追加",
|
||||
"command.context.addSelection.description": "現在のファイルから選択した行を追加",
|
||||
"command.terminal.toggle": "ターミナルの切り替え",
|
||||
@@ -70,6 +69,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "次の思考レベルに切り替え",
|
||||
"command.permissions.autoaccept.enable": "編集を自動承認",
|
||||
"command.permissions.autoaccept.disable": "編集の自動承認を停止",
|
||||
"command.workspace.toggle": "ワークスペースを切り替え",
|
||||
"command.session.undo": "元に戻す",
|
||||
"command.session.undo.description": "最後のメッセージを元に戻す",
|
||||
"command.session.redo": "やり直す",
|
||||
@@ -83,7 +83,7 @@ export const dict = {
|
||||
"command.session.unshare": "セッションの共有を停止",
|
||||
"command.session.unshare.description": "このセッションの共有を停止",
|
||||
|
||||
"palette.search.placeholder": "ファイルとコマンドを検索",
|
||||
"palette.search.placeholder": "ファイル、コマンド、セッションを検索",
|
||||
"palette.empty": "結果が見つかりません",
|
||||
"palette.group.commands": "コマンド",
|
||||
"palette.group.files": "ファイル",
|
||||
@@ -321,22 +321,6 @@ export const dict = {
|
||||
"context.usage.clickToView": "クリックしてコンテキストを表示",
|
||||
"context.usage.view": "コンテキスト使用量を表示",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "言語",
|
||||
"toast.language.description": "{{language}}に切り替えました",
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "새 세션",
|
||||
"command.file.open": "파일 열기",
|
||||
"command.file.open.description": "파일 및 명령어 검색",
|
||||
"command.context.addSelection": "선택 영역을 컨텍스트에 추가",
|
||||
"command.context.addSelection.description": "현재 파일에서 선택한 줄을 추가",
|
||||
"command.terminal.toggle": "터미널 토글",
|
||||
@@ -74,6 +73,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "다음 생각 수준으로 전환",
|
||||
"command.permissions.autoaccept.enable": "편집 자동 수락",
|
||||
"command.permissions.autoaccept.disable": "편집 자동 수락 중지",
|
||||
"command.workspace.toggle": "작업 공간 전환",
|
||||
"command.session.undo": "실행 취소",
|
||||
"command.session.undo.description": "마지막 메시지 실행 취소",
|
||||
"command.session.redo": "다시 실행",
|
||||
@@ -87,7 +87,7 @@ export const dict = {
|
||||
"command.session.unshare": "세션 공유 중지",
|
||||
"command.session.unshare.description": "이 세션 공유 중지",
|
||||
|
||||
"palette.search.placeholder": "파일 및 명령어 검색",
|
||||
"palette.search.placeholder": "파일, 명령어 및 세션 검색",
|
||||
"palette.empty": "결과 없음",
|
||||
"palette.group.commands": "명령어",
|
||||
"palette.group.files": "파일",
|
||||
@@ -324,22 +324,6 @@ export const dict = {
|
||||
"context.usage.clickToView": "컨텍스트를 보려면 클릭",
|
||||
"context.usage.view": "컨텍스트 사용량 보기",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "언어",
|
||||
"toast.language.description": "{{language}}(으)로 전환됨",
|
||||
|
||||
@@ -351,6 +335,11 @@ export const dict = {
|
||||
"toast.permissions.autoaccept.off.title": "편집 자동 수락 중지됨",
|
||||
"toast.permissions.autoaccept.off.description": "편집 및 쓰기 권한 승인이 필요합니다",
|
||||
|
||||
"toast.workspace.enabled.title": "작업 공간 활성화됨",
|
||||
"toast.workspace.enabled.description": "이제 사이드바에 여러 작업 트리가 표시됩니다",
|
||||
"toast.workspace.disabled.title": "작업 공간 비활성화됨",
|
||||
"toast.workspace.disabled.description": "사이드바에 메인 작업 트리만 표시됩니다",
|
||||
|
||||
"toast.model.none.title": "선택된 모델 없음",
|
||||
"toast.model.none.description": "이 세션을 요약하려면 공급자를 연결하세요",
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Ny sesjon",
|
||||
"command.file.open": "Åpne fil",
|
||||
"command.file.open.description": "Søk i filer og kommandoer",
|
||||
"command.context.addSelection": "Legg til markering i kontekst",
|
||||
"command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil",
|
||||
"command.terminal.toggle": "Veksle terminal",
|
||||
@@ -73,6 +72,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Bytt til neste innsatsnivå",
|
||||
"command.permissions.autoaccept.enable": "Godta endringer automatisk",
|
||||
"command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk",
|
||||
"command.workspace.toggle": "Veksle arbeidsområder",
|
||||
"command.session.undo": "Angre",
|
||||
"command.session.undo.description": "Angre siste melding",
|
||||
"command.session.redo": "Gjør om",
|
||||
@@ -86,7 +86,7 @@ export const dict = {
|
||||
"command.session.unshare": "Slutt å dele sesjon",
|
||||
"command.session.unshare.description": "Slutt å dele denne sesjonen",
|
||||
|
||||
"palette.search.placeholder": "Søk i filer og kommandoer",
|
||||
"palette.search.placeholder": "Søk i filer, kommandoer og sesjoner",
|
||||
"palette.empty": "Ingen resultater funnet",
|
||||
"palette.group.commands": "Kommandoer",
|
||||
"palette.group.files": "Filer",
|
||||
@@ -324,22 +324,6 @@ export const dict = {
|
||||
"context.usage.clickToView": "Klikk for å se kontekst",
|
||||
"context.usage.view": "Se kontekstforbruk",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Språk",
|
||||
"toast.language.description": "Byttet til {{language}}",
|
||||
|
||||
@@ -351,6 +335,11 @@ export const dict = {
|
||||
"toast.permissions.autoaccept.off.title": "Sluttet å godta endringer automatisk",
|
||||
"toast.permissions.autoaccept.off.description": "Redigerings- og skrivetillatelser vil kreve godkjenning",
|
||||
|
||||
"toast.workspace.enabled.title": "Arbeidsområder aktivert",
|
||||
"toast.workspace.enabled.description": "Flere worktrees vises nå i sidefeltet",
|
||||
"toast.workspace.disabled.title": "Arbeidsområder deaktivert",
|
||||
"toast.workspace.disabled.description": "Kun hoved-worktree vises i sidefeltet",
|
||||
|
||||
"toast.model.none.title": "Ingen modell valgt",
|
||||
"toast.model.none.description": "Koble til en leverandør for å oppsummere denne sesjonen",
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Nowa sesja",
|
||||
"command.file.open": "Otwórz plik",
|
||||
"command.file.open.description": "Szukaj plików i poleceń",
|
||||
"command.context.addSelection": "Dodaj zaznaczenie do kontekstu",
|
||||
"command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku",
|
||||
"command.terminal.toggle": "Przełącz terminal",
|
||||
@@ -70,6 +69,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku",
|
||||
"command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji",
|
||||
"command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie edycji",
|
||||
"command.workspace.toggle": "Przełącz przestrzenie robocze",
|
||||
"command.session.undo": "Cofnij",
|
||||
"command.session.undo.description": "Cofnij ostatnią wiadomość",
|
||||
"command.session.redo": "Ponów",
|
||||
@@ -83,7 +83,7 @@ export const dict = {
|
||||
"command.session.unshare": "Przestań udostępniać sesję",
|
||||
"command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji",
|
||||
|
||||
"palette.search.placeholder": "Szukaj plików i poleceń",
|
||||
"palette.search.placeholder": "Szukaj plików, poleceń i sesji",
|
||||
"palette.empty": "Brak wyników",
|
||||
"palette.group.commands": "Polecenia",
|
||||
"palette.group.files": "Pliki",
|
||||
@@ -322,22 +322,6 @@ export const dict = {
|
||||
"context.usage.clickToView": "Kliknij, aby zobaczyć kontekst",
|
||||
"context.usage.view": "Pokaż użycie kontekstu",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Język",
|
||||
"toast.language.description": "Przełączono na {{language}}",
|
||||
|
||||
@@ -349,6 +333,11 @@ export const dict = {
|
||||
"toast.permissions.autoaccept.off.title": "Zatrzymano automatyczne akceptowanie edycji",
|
||||
"toast.permissions.autoaccept.off.description": "Uprawnienia do edycji i zapisu będą wymagały zatwierdzenia",
|
||||
|
||||
"toast.workspace.enabled.title": "Przestrzenie robocze włączone",
|
||||
"toast.workspace.enabled.description": "Kilka worktree jest teraz wyświetlanych na pasku bocznym",
|
||||
"toast.workspace.disabled.title": "Przestrzenie robocze wyłączone",
|
||||
"toast.workspace.disabled.description": "Tylko główny worktree jest wyświetlany na pasku bocznym",
|
||||
|
||||
"toast.model.none.title": "Nie wybrano modelu",
|
||||
"toast.model.none.description": "Połącz dostawcę, aby podsumować tę sesję",
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Новая сессия",
|
||||
"command.file.open": "Открыть файл",
|
||||
"command.file.open.description": "Поиск файлов и команд",
|
||||
"command.context.addSelection": "Добавить выделение в контекст",
|
||||
"command.context.addSelection.description": "Добавить выбранные строки из текущего файла",
|
||||
"command.terminal.toggle": "Переключить терминал",
|
||||
@@ -70,6 +69,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "Переключиться к следующему уровню усилий",
|
||||
"command.permissions.autoaccept.enable": "Авто-принятие изменений",
|
||||
"command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений",
|
||||
"command.workspace.toggle": "Переключить рабочие пространства",
|
||||
"command.session.undo": "Отменить",
|
||||
"command.session.undo.description": "Отменить последнее сообщение",
|
||||
"command.session.redo": "Повторить",
|
||||
@@ -83,7 +83,7 @@ export const dict = {
|
||||
"command.session.unshare": "Отменить публикацию",
|
||||
"command.session.unshare.description": "Прекратить публикацию сессии",
|
||||
|
||||
"palette.search.placeholder": "Поиск файлов и команд",
|
||||
"palette.search.placeholder": "Поиск файлов, команд и сессий",
|
||||
"palette.empty": "Ничего не найдено",
|
||||
"palette.group.commands": "Команды",
|
||||
"palette.group.files": "Файлы",
|
||||
@@ -323,22 +323,6 @@ export const dict = {
|
||||
"context.usage.clickToView": "Нажмите для просмотра контекста",
|
||||
"context.usage.view": "Показать использование контекста",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "Язык",
|
||||
"toast.language.description": "Переключено на {{language}}",
|
||||
|
||||
@@ -350,6 +334,11 @@ export const dict = {
|
||||
"toast.permissions.autoaccept.off.title": "Авто-принятие остановлено",
|
||||
"toast.permissions.autoaccept.off.description": "Редактирование и запись потребуют подтверждения",
|
||||
|
||||
"toast.workspace.enabled.title": "Рабочие пространства включены",
|
||||
"toast.workspace.enabled.description": "В боковой панели теперь отображаются несколько рабочих деревьев",
|
||||
"toast.workspace.disabled.title": "Рабочие пространства отключены",
|
||||
"toast.workspace.disabled.description": "В боковой панели отображается только главное рабочее дерево",
|
||||
|
||||
"toast.model.none.title": "Модель не выбрана",
|
||||
"toast.model.none.description": "Подключите провайдера для суммаризации сессии",
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "เซสชันใหม่",
|
||||
"command.file.open": "เปิดไฟล์",
|
||||
"command.file.open.description": "ค้นหาไฟล์และคำสั่ง",
|
||||
"command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท",
|
||||
"command.context.addSelection.description": "เพิ่มบรรทัดที่เลือกจากไฟล์ปัจจุบัน",
|
||||
"command.terminal.toggle": "สลับเทอร์มินัล",
|
||||
@@ -70,6 +69,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
|
||||
"command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||
"command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||
"command.workspace.toggle": "สลับพื้นที่ทำงาน",
|
||||
"command.session.undo": "ยกเลิก",
|
||||
"command.session.undo.description": "ยกเลิกข้อความล่าสุด",
|
||||
"command.session.redo": "ทำซ้ำ",
|
||||
@@ -83,7 +83,7 @@ export const dict = {
|
||||
"command.session.unshare": "ยกเลิกการแชร์เซสชัน",
|
||||
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
|
||||
|
||||
"palette.search.placeholder": "ค้นหาไฟล์และคำสั่ง",
|
||||
"palette.search.placeholder": "ค้นหาไฟล์ คำสั่ง และเซสชัน",
|
||||
"palette.empty": "ไม่พบผลลัพธ์",
|
||||
"palette.group.commands": "คำสั่ง",
|
||||
"palette.group.files": "ไฟล์",
|
||||
@@ -326,22 +326,6 @@ export const dict = {
|
||||
"context.usage.clickToView": "คลิกเพื่อดูบริบท",
|
||||
"context.usage.view": "ดูการใช้บริบท",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "ภาษา",
|
||||
"toast.language.description": "สลับไปที่ {{language}}",
|
||||
|
||||
@@ -349,10 +333,15 @@ export const dict = {
|
||||
"toast.scheme.title": "โทนสี",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "กำลังยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||
"toast.permissions.autoaccept.on.description": "สิทธิ์การแก้ไขและเขียนจะได้รับการอนุมัติโดยอัตโนมัติ",
|
||||
"toast.permissions.autoaccept.on.description": "สิทธิ์การแก้ไขและจะได้รับเขียนการอนุมัติโดยอัตโนมัติ",
|
||||
"toast.permissions.autoaccept.off.title": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||
"toast.permissions.autoaccept.off.description": "สิทธิ์การแก้ไขและเขียนจะต้องได้รับการอนุมัติ",
|
||||
|
||||
"toast.workspace.enabled.title": "เปิดใช้งานพื้นที่ทำงานแล้ว",
|
||||
"toast.workspace.enabled.description": "ตอนนี้จะแสดง worktree หลายรายการในแถบด้านข้าง",
|
||||
"toast.workspace.disabled.title": "ปิดใช้งานพื้นที่ทำงานแล้ว",
|
||||
"toast.workspace.disabled.description": "จะแสดงเฉพาะ worktree หลักในแถบด้านข้าง",
|
||||
|
||||
"toast.model.none.title": "ไม่ได้เลือกโมเดล",
|
||||
"toast.model.none.description": "เชื่อมต่อผู้ให้บริการเพื่อสรุปเซสชันนี้",
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "新建会话",
|
||||
"command.file.open": "打开文件",
|
||||
"command.file.open.description": "搜索文件和命令",
|
||||
"command.context.addSelection": "将所选内容添加到上下文",
|
||||
"command.context.addSelection.description": "添加当前文件中选中的行",
|
||||
"command.terminal.toggle": "切换终端",
|
||||
@@ -74,6 +73,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "切换到下一个强度等级",
|
||||
"command.permissions.autoaccept.enable": "自动接受编辑",
|
||||
"command.permissions.autoaccept.disable": "停止自动接受编辑",
|
||||
"command.workspace.toggle": "切换工作区",
|
||||
"command.session.undo": "撤销",
|
||||
"command.session.undo.description": "撤销上一条消息",
|
||||
"command.session.redo": "重做",
|
||||
@@ -87,7 +87,7 @@ export const dict = {
|
||||
"command.session.unshare": "取消分享会话",
|
||||
"command.session.unshare.description": "停止分享此会话",
|
||||
|
||||
"palette.search.placeholder": "搜索文件和命令",
|
||||
"palette.search.placeholder": "搜索文件、命令和会话",
|
||||
"palette.empty": "未找到结果",
|
||||
"palette.group.commands": "命令",
|
||||
"palette.group.files": "文件",
|
||||
@@ -324,27 +324,16 @@ export const dict = {
|
||||
"context.usage.clickToView": "点击查看上下文",
|
||||
"context.usage.view": "查看上下文用量",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "语言",
|
||||
"toast.language.description": "已切换到{{language}}",
|
||||
|
||||
"toast.theme.title": "主题已切换",
|
||||
"toast.scheme.title": "配色方案",
|
||||
"toast.scheme.title": "颜色方案",
|
||||
|
||||
"toast.workspace.enabled.title": "工作区已启用",
|
||||
"toast.workspace.enabled.description": "侧边栏现在显示多个工作树",
|
||||
"toast.workspace.disabled.title": "工作区已禁用",
|
||||
"toast.workspace.disabled.description": "侧边栏只显示主工作树",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "自动接受编辑",
|
||||
"toast.permissions.autoaccept.on.description": "编辑和写入权限将自动获批",
|
||||
|
||||
@@ -48,7 +48,6 @@ export const dict = {
|
||||
|
||||
"command.session.new": "新增工作階段",
|
||||
"command.file.open": "開啟檔案",
|
||||
"command.file.open.description": "搜尋檔案和命令",
|
||||
"command.context.addSelection": "將選取內容加入上下文",
|
||||
"command.context.addSelection.description": "加入目前檔案中選取的行",
|
||||
"command.terminal.toggle": "切換終端機",
|
||||
@@ -74,6 +73,7 @@ export const dict = {
|
||||
"command.model.variant.cycle.description": "切換到下一個強度等級",
|
||||
"command.permissions.autoaccept.enable": "自動接受編輯",
|
||||
"command.permissions.autoaccept.disable": "停止自動接受編輯",
|
||||
"command.workspace.toggle": "切換工作區",
|
||||
"command.session.undo": "復原",
|
||||
"command.session.undo.description": "復原上一則訊息",
|
||||
"command.session.redo": "重做",
|
||||
@@ -87,7 +87,7 @@ export const dict = {
|
||||
"command.session.unshare": "取消分享工作階段",
|
||||
"command.session.unshare.description": "停止分享此工作階段",
|
||||
|
||||
"palette.search.placeholder": "搜尋檔案和命令",
|
||||
"palette.search.placeholder": "搜尋檔案、命令和工作階段",
|
||||
"palette.empty": "找不到結果",
|
||||
"palette.group.commands": "命令",
|
||||
"palette.group.files": "檔案",
|
||||
@@ -321,27 +321,16 @@ export const dict = {
|
||||
"context.usage.clickToView": "點擊查看上下文",
|
||||
"context.usage.view": "檢視上下文用量",
|
||||
|
||||
"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.th": "ไทย",
|
||||
|
||||
"toast.language.title": "語言",
|
||||
"toast.language.description": "已切換到 {{language}}",
|
||||
|
||||
"toast.theme.title": "主題已切換",
|
||||
"toast.scheme.title": "配色方案",
|
||||
"toast.scheme.title": "顏色方案",
|
||||
|
||||
"toast.workspace.enabled.title": "工作區已啟用",
|
||||
"toast.workspace.enabled.description": "側邊欄現在顯示多個工作樹",
|
||||
"toast.workspace.disabled.title": "工作區已停用",
|
||||
"toast.workspace.disabled.description": "側邊欄只顯示主工作樹",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "自動接受編輯",
|
||||
"toast.permissions.autoaccept.on.description": "編輯和寫入權限將自動獲准",
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function Layout(props: ParentProps) {
|
||||
})
|
||||
return (
|
||||
<Show when={directory()}>
|
||||
<SDKProvider directory={directory()}>
|
||||
<SDKProvider directory={directory}>
|
||||
<SyncProvider>
|
||||
{iife(() => {
|
||||
const sync = useSync()
|
||||
|
||||
+335
-317
@@ -31,6 +31,7 @@ import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { HoverCard } from "@opencode-ai/ui/hover-card"
|
||||
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { ContextMenu } from "@opencode-ai/ui/context-menu"
|
||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
@@ -57,6 +58,7 @@ import { usePermission } from "@/context/permission"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { retry } from "@opencode-ai/util/retry"
|
||||
import { playSound, soundSrc } from "@/utils/sound"
|
||||
import { createAim } from "@/utils/aim"
|
||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||
import { agentColor } from "@/utils/agent"
|
||||
|
||||
@@ -108,7 +110,7 @@ export default function Layout(props: ParentProps) {
|
||||
const command = useCommand()
|
||||
const theme = useTheme()
|
||||
const language = useLanguage()
|
||||
const initialDir = params.dir
|
||||
const initialDirectory = decode64(params.dir)
|
||||
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
|
||||
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
|
||||
const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = {
|
||||
@@ -119,7 +121,7 @@ export default function Layout(props: ParentProps) {
|
||||
const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
|
||||
|
||||
const [state, setState] = createStore({
|
||||
autoselect: !params.dir,
|
||||
autoselect: !initialDirectory,
|
||||
busyWorkspaces: new Set<string>(),
|
||||
hoverSession: undefined as string | undefined,
|
||||
hoverProject: undefined as string | undefined,
|
||||
@@ -145,9 +147,20 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
const navLeave = { current: undefined as number | undefined }
|
||||
|
||||
const aim = createAim({
|
||||
enabled: () => !layout.sidebar.opened(),
|
||||
active: () => state.hoverProject,
|
||||
el: () => state.nav,
|
||||
onActivate: (directory) => {
|
||||
globalSync.child(directory)
|
||||
setState("hoverProject", directory)
|
||||
setState("hoverSession", undefined)
|
||||
},
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (navLeave.current === undefined) return
|
||||
clearTimeout(navLeave.current)
|
||||
if (navLeave.current !== undefined) clearTimeout(navLeave.current)
|
||||
aim.reset()
|
||||
})
|
||||
|
||||
const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined)
|
||||
@@ -161,15 +174,22 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
createEffect(() => {
|
||||
if (!layout.sidebar.opened()) return
|
||||
aim.reset()
|
||||
setState("hoverProject", undefined)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (state.hoverProject !== undefined) return
|
||||
aim.reset()
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => ({ dir: params.dir, id: params.id }),
|
||||
() => {
|
||||
if (layout.sidebar.opened()) return
|
||||
if (!state.hoverProject) return
|
||||
aim.reset()
|
||||
setState("hoverSession", undefined)
|
||||
setState("hoverProject", undefined)
|
||||
},
|
||||
@@ -179,13 +199,21 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
const autoselecting = createMemo(() => {
|
||||
if (params.dir) return false
|
||||
if (initialDir) return false
|
||||
if (!state.autoselect) return false
|
||||
if (!pageReady()) return true
|
||||
if (!layoutReady()) return true
|
||||
const list = layout.projects.list()
|
||||
if (list.length === 0) return false
|
||||
return true
|
||||
if (list.length > 0) return true
|
||||
return !!server.projects.last()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!state.autoselect) return
|
||||
const dir = params.dir
|
||||
if (!dir) return
|
||||
const directory = decode64(dir)
|
||||
if (!directory) return
|
||||
setState("autoselect", false)
|
||||
})
|
||||
|
||||
const editorOpen = (id: string) => editor.active === id
|
||||
@@ -498,7 +526,7 @@ export default function Layout(props: ParentProps) {
|
||||
const bUpdated = b.time.updated ?? b.time.created
|
||||
const aRecent = aUpdated > oneMinuteAgo
|
||||
const bRecent = bUpdated > oneMinuteAgo
|
||||
if (aRecent && bRecent) return a.id.localeCompare(b.id)
|
||||
if (aRecent && bRecent) return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
||||
if (aRecent && !bRecent) return -1
|
||||
if (!aRecent && bRecent) return 1
|
||||
return bUpdated - aUpdated
|
||||
@@ -565,11 +593,18 @@ export default function Layout(props: ParentProps) {
|
||||
if (!value.ready) return
|
||||
if (!value.layoutReady) return
|
||||
if (!state.autoselect) return
|
||||
if (initialDir) return
|
||||
if (value.dir) return
|
||||
if (value.list.length === 0) return
|
||||
|
||||
const last = server.projects.last()
|
||||
|
||||
if (value.list.length === 0) {
|
||||
if (!last) return
|
||||
setState("autoselect", false)
|
||||
openProject(last, false)
|
||||
navigateToProject(last)
|
||||
return
|
||||
}
|
||||
|
||||
const next = value.list.find((project) => project.worktree === last) ?? value.list[0]
|
||||
if (!next) return
|
||||
setState("autoselect", false)
|
||||
@@ -738,7 +773,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
async function prefetchMessages(directory: string, sessionID: string, token: number) {
|
||||
const [, setStore] = globalSync.child(directory, { bootstrap: false })
|
||||
const [store, setStore] = globalSync.child(directory, { bootstrap: false })
|
||||
|
||||
return retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
|
||||
.then((messages) => {
|
||||
@@ -749,23 +784,49 @@ export default function Layout(props: ParentProps) {
|
||||
.map((x) => x.info)
|
||||
.filter((m) => !!m?.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
|
||||
const current = store.message[sessionID] ?? []
|
||||
const merged = (() => {
|
||||
if (current.length === 0) return next
|
||||
|
||||
const map = new Map<string, Message>()
|
||||
for (const item of current) {
|
||||
if (!item?.id) continue
|
||||
map.set(item.id, item)
|
||||
}
|
||||
for (const item of next) {
|
||||
map.set(item.id, item)
|
||||
}
|
||||
return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
})()
|
||||
|
||||
batch(() => {
|
||||
setStore("message", sessionID, reconcile(next, { key: "id" }))
|
||||
setStore("message", sessionID, reconcile(merged, { key: "id" }))
|
||||
|
||||
for (const message of items) {
|
||||
setStore(
|
||||
"part",
|
||||
message.info.id,
|
||||
reconcile(
|
||||
message.parts
|
||||
const currentParts = store.part[message.info.id] ?? []
|
||||
const mergedParts = (() => {
|
||||
if (currentParts.length === 0) {
|
||||
return message.parts
|
||||
.filter((p) => !!p?.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
}
|
||||
|
||||
const map = new Map<string, (typeof currentParts)[number]>()
|
||||
for (const item of currentParts) {
|
||||
if (!item?.id) continue
|
||||
map.set(item.id, item)
|
||||
}
|
||||
for (const item of message.parts) {
|
||||
if (!item?.id) continue
|
||||
map.set(item.id, item)
|
||||
}
|
||||
return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
})()
|
||||
|
||||
setStore("part", message.info.id, reconcile(mergedParts, { key: "id" }))
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -958,69 +1019,6 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession(session: Session) {
|
||||
const [store, setStore] = globalSync.child(session.directory)
|
||||
const sessions = (store.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
|
||||
const index = sessions.findIndex((s) => s.id === session.id)
|
||||
const nextSession = sessions[index + 1] ?? sessions[index - 1]
|
||||
|
||||
const result = await globalSDK.client.session
|
||||
.delete({ directory: session.directory, sessionID: session.id })
|
||||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("session.delete.failed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
if (!result) return
|
||||
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const removed = new Set<string>([session.id])
|
||||
|
||||
const byParent = new Map<string, string[]>()
|
||||
for (const item of draft.session) {
|
||||
const parentID = item.parentID
|
||||
if (!parentID) continue
|
||||
const existing = byParent.get(parentID)
|
||||
if (existing) {
|
||||
existing.push(item.id)
|
||||
continue
|
||||
}
|
||||
byParent.set(parentID, [item.id])
|
||||
}
|
||||
|
||||
const stack = [session.id]
|
||||
while (stack.length) {
|
||||
const parentID = stack.pop()
|
||||
if (!parentID) continue
|
||||
|
||||
const children = byParent.get(parentID)
|
||||
if (!children) continue
|
||||
|
||||
for (const child of children) {
|
||||
if (removed.has(child)) continue
|
||||
removed.add(child)
|
||||
stack.push(child)
|
||||
}
|
||||
}
|
||||
|
||||
draft.session = draft.session.filter((s) => !removed.has(s.id))
|
||||
}),
|
||||
)
|
||||
|
||||
if (session.id === params.id) {
|
||||
if (nextSession) {
|
||||
navigate(`/${params.dir}/session/${nextSession.id}`)
|
||||
} else {
|
||||
navigate(`/${params.dir}/session`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
command.register(() => {
|
||||
const commands: CommandOption[] = [
|
||||
{
|
||||
@@ -1095,6 +1093,29 @@ export default function Layout(props: ParentProps) {
|
||||
if (session) archiveSession(session)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "workspace.toggle",
|
||||
title: language.t("command.workspace.toggle"),
|
||||
description: language.t("command.workspace.toggle.description"),
|
||||
category: language.t("command.category.workspace"),
|
||||
slash: "workspace",
|
||||
disabled: !currentProject() || currentProject()?.vcs !== "git",
|
||||
onSelect: () => {
|
||||
const project = currentProject()
|
||||
if (!project) return
|
||||
if (project.vcs !== "git") return
|
||||
const wasEnabled = layout.sidebar.workspaces(project.worktree)()
|
||||
layout.sidebar.toggleWorkspaces(project.worktree)
|
||||
showToast({
|
||||
title: wasEnabled
|
||||
? language.t("toast.workspace.disabled.title")
|
||||
: language.t("toast.workspace.enabled.title"),
|
||||
description: wasEnabled
|
||||
? language.t("toast.workspace.disabled.description")
|
||||
: language.t("toast.workspace.enabled.description"),
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "theme.cycle",
|
||||
title: language.t("command.theme.cycle"),
|
||||
@@ -1251,15 +1272,6 @@ export default function Layout(props: ParentProps) {
|
||||
globalSync.project.meta(project.worktree, { name })
|
||||
}
|
||||
|
||||
async function renameSession(session: Session, next: string) {
|
||||
if (next === session.title) return
|
||||
await globalSDK.client.session.update({
|
||||
directory: session.directory,
|
||||
sessionID: session.id,
|
||||
title: next,
|
||||
})
|
||||
}
|
||||
|
||||
const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
|
||||
const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
|
||||
if (current === next) return
|
||||
@@ -1410,33 +1422,6 @@ export default function Layout(props: ParentProps) {
|
||||
})
|
||||
}
|
||||
|
||||
function DialogDeleteSession(props: { session: Session }) {
|
||||
const handleDelete = async () => {
|
||||
await deleteSession(props.session)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={language.t("session.delete.title")} fit>
|
||||
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-14-regular text-text-strong">
|
||||
{language.t("session.delete.confirm", { name: props.session.title })}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||
{language.t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" size="large" onClick={handleDelete}>
|
||||
{language.t("session.delete.button")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDeleteWorkspace(props: { root: string; directory: string }) {
|
||||
const name = createMemo(() => getFilename(props.directory))
|
||||
const [data, setData] = createStore({
|
||||
@@ -1790,10 +1775,6 @@ export default function Layout(props: ParentProps) {
|
||||
const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded())
|
||||
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
||||
const isActive = createMemo(() => props.session.id === params.id)
|
||||
const [menu, setMenu] = createStore({
|
||||
open: false,
|
||||
pendingRename: false,
|
||||
})
|
||||
|
||||
const hoverPrefetch = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
||||
const cancelHoverPrefetch = () => {
|
||||
@@ -1820,7 +1801,7 @@ export default function Layout(props: ParentProps) {
|
||||
const item = (
|
||||
<A
|
||||
href={`${props.slug}/session/${props.session.id}`}
|
||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${menu.open ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
onPointerEnter={scheduleHoverPrefetch}
|
||||
onPointerLeave={cancelHoverPrefetch}
|
||||
onMouseEnter={scheduleHoverPrefetch}
|
||||
@@ -1852,14 +1833,9 @@ export default function Layout(props: ParentProps) {
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<InlineEditor
|
||||
id={`session:${props.session.id}`}
|
||||
value={() => props.session.title}
|
||||
onSave={(next) => renameSession(props.session, next)}
|
||||
class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
|
||||
displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
|
||||
stopPropagation
|
||||
/>
|
||||
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
||||
{props.session.title}
|
||||
</span>
|
||||
<Show when={props.session.summary}>
|
||||
{(summary) => (
|
||||
<div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
||||
@@ -1907,7 +1883,10 @@ export default function Layout(props: ParentProps) {
|
||||
getLabel={messageLabel}
|
||||
onMessageSelect={(message) => {
|
||||
if (!isActive()) {
|
||||
sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`)
|
||||
layout.pendingMessage.set(
|
||||
`${base64Encode(props.session.directory)}/${props.session.id}`,
|
||||
message.id,
|
||||
)
|
||||
navigate(`${props.slug}/session/${props.session.id}`)
|
||||
return
|
||||
}
|
||||
@@ -1924,49 +1903,25 @@ export default function Layout(props: ParentProps) {
|
||||
<div
|
||||
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
|
||||
classList={{
|
||||
"opacity-100 pointer-events-auto": menu.open,
|
||||
"opacity-0 pointer-events-none": !menu.open,
|
||||
"opacity-100 pointer-events-auto": !!props.mobile,
|
||||
"opacity-0 pointer-events-none": !props.mobile,
|
||||
"group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
|
||||
"group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
|
||||
}}
|
||||
>
|
||||
<DropdownMenu modal={!sidebarHovering()} open={menu.open} onOpenChange={(open) => setMenu("open", open)}>
|
||||
<Tooltip value={language.t("common.moreOptions")} placement="top">
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||
aria-label={language.t("common.moreOptions")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DropdownMenu.Portal mount={!props.mobile ? state.nav : undefined}>
|
||||
<DropdownMenu.Content
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (!menu.pendingRename) return
|
||||
event.preventDefault()
|
||||
setMenu("pendingRename", false)
|
||||
openEditor(`session:${props.session.id}`, props.session.title)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
setMenu("pendingRename", true)
|
||||
setMenu("open", false)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => archiveSession(props.session)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogDeleteSession session={props.session} />)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
<Tooltip value={language.t("common.archive")} placement="top">
|
||||
<IconButton
|
||||
icon="archive"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md"
|
||||
aria-label={language.t("common.archive")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
void archiveSession(props.session)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -2100,9 +2055,10 @@ export default function Layout(props: ParentProps) {
|
||||
const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local())
|
||||
const boot = createMemo(() => open() || active())
|
||||
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
|
||||
const loading = createMemo(() => open() && !booted() && sessions().length === 0)
|
||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
|
||||
const busy = createMemo(() => isBusy(props.directory))
|
||||
const wasBusy = createMemo((prev) => prev || busy(), false)
|
||||
const loading = createMemo(() => open() && !booted() && sessions().length === 0 && !wasBusy())
|
||||
const loadMore = async () => {
|
||||
setWorkspaceStore("limit", (limit) => limit + 5)
|
||||
await globalSync.project.loadSessions(props.directory)
|
||||
@@ -2310,10 +2266,13 @@ export default function Layout(props: ParentProps) {
|
||||
() => props.project.vcs === "git" && layout.sidebar.workspaces(props.project.worktree)(),
|
||||
)
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const [menu, setMenu] = createSignal(false)
|
||||
|
||||
const preview = createMemo(() => !props.mobile && layout.sidebar.opened())
|
||||
const overlay = createMemo(() => !props.mobile && !layout.sidebar.opened())
|
||||
const active = createMemo(() => (preview() ? open() : overlay() && state.hoverProject === props.project.worktree))
|
||||
const active = createMemo(
|
||||
() => menu() || (preview() ? open() : overlay() && state.hoverProject === props.project.worktree),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
if (preview()) return
|
||||
@@ -2351,50 +2310,95 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
const projectName = () => props.project.name || getFilename(props.project.worktree)
|
||||
const trigger = (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={projectName()}
|
||||
data-action="project-switch"
|
||||
data-project={base64Encode(props.project.worktree)}
|
||||
classList={{
|
||||
"flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
||||
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": selected(),
|
||||
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
||||
!selected() && !active(),
|
||||
"bg-surface-base-hover border border-border-weak-base": !selected() && active(),
|
||||
const Trigger = () => (
|
||||
<ContextMenu
|
||||
modal={!sidebarHovering()}
|
||||
onOpenChange={(value) => {
|
||||
setMenu(value)
|
||||
if (value) setOpen(false)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (!overlay()) return
|
||||
globalSync.child(props.project.worktree)
|
||||
setState("hoverProject", props.project.worktree)
|
||||
setState("hoverSession", undefined)
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (!overlay()) return
|
||||
globalSync.child(props.project.worktree)
|
||||
setState("hoverProject", props.project.worktree)
|
||||
setState("hoverSession", undefined)
|
||||
}}
|
||||
onClick={() => navigateToProject(props.project.worktree)}
|
||||
onBlur={() => setOpen(false)}
|
||||
>
|
||||
<ProjectIcon project={props.project} notify />
|
||||
</button>
|
||||
<ContextMenu.Trigger
|
||||
as="button"
|
||||
type="button"
|
||||
aria-label={projectName()}
|
||||
data-action="project-switch"
|
||||
data-project={base64Encode(props.project.worktree)}
|
||||
classList={{
|
||||
"flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
||||
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": selected(),
|
||||
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
||||
!selected() && !active(),
|
||||
"bg-surface-base-hover border border-border-weak-base": !selected() && active(),
|
||||
}}
|
||||
onMouseEnter={(event: MouseEvent) => {
|
||||
if (!overlay()) return
|
||||
aim.enter(props.project.worktree, event)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (!overlay()) return
|
||||
aim.leave(props.project.worktree)
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (!overlay()) return
|
||||
aim.activate(props.project.worktree)
|
||||
}}
|
||||
onClick={() => navigateToProject(props.project.worktree)}
|
||||
onBlur={() => setOpen(false)}
|
||||
>
|
||||
<ProjectIcon project={props.project} notify />
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal mount={!props.mobile ? state.nav : undefined}>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={props.project} />)}>
|
||||
<ContextMenu.ItemLabel>{language.t("common.edit")}</ContextMenu.ItemLabel>
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item
|
||||
data-action="project-workspaces-toggle"
|
||||
data-project={base64Encode(props.project.worktree)}
|
||||
disabled={props.project.vcs !== "git" && !layout.sidebar.workspaces(props.project.worktree)()}
|
||||
onSelect={() => {
|
||||
const enabled = layout.sidebar.workspaces(props.project.worktree)()
|
||||
if (enabled) {
|
||||
layout.sidebar.toggleWorkspaces(props.project.worktree)
|
||||
return
|
||||
}
|
||||
if (props.project.vcs !== "git") return
|
||||
layout.sidebar.toggleWorkspaces(props.project.worktree)
|
||||
}}
|
||||
>
|
||||
<ContextMenu.ItemLabel>
|
||||
{layout.sidebar.workspaces(props.project.worktree)()
|
||||
? language.t("sidebar.workspaces.disable")
|
||||
: language.t("sidebar.workspaces.enable")}
|
||||
</ContextMenu.ItemLabel>
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item
|
||||
data-action="project-close-menu"
|
||||
data-project={base64Encode(props.project.worktree)}
|
||||
onSelect={() => closeProject(props.project.worktree)}
|
||||
>
|
||||
<ContextMenu.ItemLabel>{language.t("common.close")}</ContextMenu.ItemLabel>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
)
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||
<Show when={preview()} fallback={trigger}>
|
||||
<Show when={preview()} fallback={<Trigger />}>
|
||||
<HoverCard
|
||||
open={open()}
|
||||
open={open() && !menu()}
|
||||
openDelay={0}
|
||||
closeDelay={0}
|
||||
placement="right-start"
|
||||
gutter={6}
|
||||
trigger={trigger}
|
||||
trigger={<Trigger />}
|
||||
onOpenChange={(value) => {
|
||||
if (menu()) return
|
||||
setOpen(value)
|
||||
if (value) setState("hoverSession", undefined)
|
||||
}}
|
||||
@@ -2485,17 +2489,22 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
const LocalWorkspace = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
|
||||
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.project.worktree)
|
||||
const workspace = createMemo(() => {
|
||||
const [store, setStore] = globalSync.child(props.project.worktree)
|
||||
return { store, setStore }
|
||||
})
|
||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||
const sessions = createMemo(() =>
|
||||
workspaceStore.session
|
||||
.filter((session) => session.directory === workspaceStore.path.directory)
|
||||
const sessions = createMemo(() => {
|
||||
const store = workspace().store
|
||||
return store.session
|
||||
.filter((session) => session.directory === store.path.directory)
|
||||
.filter((session) => !session.parentID && !session.time?.archived)
|
||||
.toSorted(sortSessions(Date.now())),
|
||||
)
|
||||
.toSorted(sortSessions(Date.now()))
|
||||
})
|
||||
const children = createMemo(() => {
|
||||
const store = workspace().store
|
||||
const map = new Map<string, string[]>()
|
||||
for (const session of workspaceStore.session) {
|
||||
for (const session of store.session) {
|
||||
if (!session.parentID) continue
|
||||
const existing = map.get(session.parentID)
|
||||
if (existing) {
|
||||
@@ -2506,11 +2515,11 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
return map
|
||||
})
|
||||
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
|
||||
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
|
||||
const loading = createMemo(() => !booted() && sessions().length === 0)
|
||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
|
||||
const hasMore = createMemo(() => workspace().store.sessionTotal > sessions().length)
|
||||
const loadMore = async () => {
|
||||
setWorkspaceStore("limit", (limit) => limit + 5)
|
||||
workspace().setStore("limit", (limit) => limit + 5)
|
||||
await globalSync.project.loadSessions(props.project.worktree)
|
||||
}
|
||||
|
||||
@@ -2566,6 +2575,8 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
if (!created?.directory) return
|
||||
|
||||
setWorkspaceName(created.directory, created.branch, project.id, created.branch)
|
||||
|
||||
const local = project.worktree
|
||||
const key = workspaceKey(created.directory)
|
||||
const root = workspaceKey(local)
|
||||
@@ -2615,7 +2626,7 @@ export default function Layout(props: ParentProps) {
|
||||
}}
|
||||
style={{ width: panelProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }}
|
||||
>
|
||||
<Show when={panelProps.project} keyed>
|
||||
<Show when={panelProps.project}>
|
||||
{(p) => (
|
||||
<>
|
||||
<div class="shrink-0 px-2 py-1">
|
||||
@@ -2624,7 +2635,7 @@ export default function Layout(props: ParentProps) {
|
||||
<InlineEditor
|
||||
id={`project:${projectId()}`}
|
||||
value={projectName}
|
||||
onSave={(next) => renameProject(p, next)}
|
||||
onSave={(next) => renameProject(p(), next)}
|
||||
class="text-16-medium text-text-strong truncate"
|
||||
displayClass="text-16-medium text-text-strong truncate"
|
||||
stopPropagation
|
||||
@@ -2633,7 +2644,7 @@ export default function Layout(props: ParentProps) {
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
gutter={2}
|
||||
value={p.worktree}
|
||||
value={p().worktree}
|
||||
class="shrink-0"
|
||||
contentStyle={{
|
||||
"max-width": "640px",
|
||||
@@ -2641,7 +2652,7 @@ export default function Layout(props: ParentProps) {
|
||||
}}
|
||||
>
|
||||
<span class="text-12-regular text-text-base truncate select-text">
|
||||
{p.worktree.replace(homedir(), "~")}
|
||||
{p().worktree.replace(homedir(), "~")}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -2652,31 +2663,31 @@ export default function Layout(props: ParentProps) {
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
data-action="project-menu"
|
||||
data-project={base64Encode(p.worktree)}
|
||||
data-project={base64Encode(p().worktree)}
|
||||
class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active"
|
||||
aria-label={language.t("common.moreOptions")}
|
||||
/>
|
||||
<DropdownMenu.Portal mount={!panelProps.mobile ? state.nav : undefined}>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p} />)}>
|
||||
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p()} />)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
data-action="project-workspaces-toggle"
|
||||
data-project={base64Encode(p.worktree)}
|
||||
disabled={p.vcs !== "git" && !layout.sidebar.workspaces(p.worktree)()}
|
||||
data-project={base64Encode(p().worktree)}
|
||||
disabled={p().vcs !== "git" && !layout.sidebar.workspaces(p().worktree)()}
|
||||
onSelect={() => {
|
||||
const enabled = layout.sidebar.workspaces(p.worktree)()
|
||||
const enabled = layout.sidebar.workspaces(p().worktree)()
|
||||
if (enabled) {
|
||||
layout.sidebar.toggleWorkspaces(p.worktree)
|
||||
layout.sidebar.toggleWorkspaces(p().worktree)
|
||||
return
|
||||
}
|
||||
if (p.vcs !== "git") return
|
||||
layout.sidebar.toggleWorkspaces(p.worktree)
|
||||
if (p().vcs !== "git") return
|
||||
layout.sidebar.toggleWorkspaces(p().worktree)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{layout.sidebar.workspaces(p.worktree)()
|
||||
{layout.sidebar.workspaces(p().worktree)()
|
||||
? language.t("sidebar.workspaces.disable")
|
||||
: language.t("sidebar.workspaces.enable")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
@@ -2684,8 +2695,8 @@ export default function Layout(props: ParentProps) {
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
data-action="project-close-menu"
|
||||
data-project={base64Encode(p.worktree)}
|
||||
onSelect={() => closeProject(p.worktree)}
|
||||
data-project={base64Encode(p().worktree)}
|
||||
onSelect={() => closeProject(p().worktree)}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
@@ -2695,103 +2706,109 @@ export default function Layout(props: ParentProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={workspacesEnabled()}
|
||||
fallback={
|
||||
<div class="flex-1 min-h-0 flex flex-col">
|
||||
<Show
|
||||
when={workspacesEnabled()}
|
||||
fallback={
|
||||
<>
|
||||
<div class="shrink-0 py-4 px-3">
|
||||
<TooltipKeybind
|
||||
title={language.t("command.session.new")}
|
||||
keybind={command.keybind("session.new")}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
onClick={() => {
|
||||
if (!layout.sidebar.opened()) {
|
||||
setState("hoverSession", undefined)
|
||||
setState("hoverProject", undefined)
|
||||
}
|
||||
navigate(`/${base64Encode(p().worktree)}/session`)
|
||||
layout.mobileSidebar.hide()
|
||||
}}
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<LocalWorkspace project={p()} mobile={panelProps.mobile} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<div class="py-4 px-3">
|
||||
<div class="shrink-0 py-4 px-3">
|
||||
<TooltipKeybind
|
||||
title={language.t("command.session.new")}
|
||||
keybind={command.keybind("session.new")}
|
||||
title={language.t("workspace.new")}
|
||||
keybind={command.keybind("workspace.new")}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
onClick={() => {
|
||||
if (!layout.sidebar.opened()) {
|
||||
setState("hoverSession", undefined)
|
||||
setState("hoverProject", undefined)
|
||||
}
|
||||
navigate(`/${base64Encode(p.worktree)}/session`)
|
||||
layout.mobileSidebar.hide()
|
||||
}}
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}>
|
||||
{language.t("workspace.new")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<LocalWorkspace project={p} mobile={panelProps.mobile} />
|
||||
<div class="relative flex-1 min-h-0">
|
||||
<DragDropProvider
|
||||
onDragStart={handleWorkspaceDragStart}
|
||||
onDragEnd={handleWorkspaceDragEnd}
|
||||
onDragOver={handleWorkspaceDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragXAxis />
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (!panelProps.mobile) scrollContainerRef = el
|
||||
}}
|
||||
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||
>
|
||||
<SortableProvider ids={workspaces()}>
|
||||
<For each={workspaces()}>
|
||||
{(directory) => (
|
||||
<SortableWorkspace directory={directory} project={p()} mobile={panelProps.mobile} />
|
||||
)}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
<WorkspaceDragOverlay />
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<div class="py-4 px-3">
|
||||
<TooltipKeybind
|
||||
title={language.t("workspace.new")}
|
||||
keybind={command.keybind("workspace.new")}
|
||||
placement="top"
|
||||
>
|
||||
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p)}>
|
||||
{language.t("workspace.new")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<div class="relative flex-1 min-h-0">
|
||||
<DragDropProvider
|
||||
onDragStart={handleWorkspaceDragStart}
|
||||
onDragEnd={handleWorkspaceDragEnd}
|
||||
onDragOver={handleWorkspaceDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragXAxis />
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (!panelProps.mobile) scrollContainerRef = el
|
||||
}}
|
||||
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||
>
|
||||
<SortableProvider ids={workspaces()}>
|
||||
<For each={workspaces()}>
|
||||
{(directory) => (
|
||||
<SortableWorkspace directory={directory} project={p} mobile={panelProps.mobile} />
|
||||
)}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
<WorkspaceDragOverlay />
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
</>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={providers.all().length > 0 && providers.paid().length === 0}>
|
||||
<div class="shrink-0 px-2 py-3 border-t border-border-weak-base">
|
||||
<div class="rounded-md bg-background-base shadow-xs-border-base">
|
||||
<div class="p-3 flex flex-col gap-2">
|
||||
<div class="text-12-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div>
|
||||
<div class="text-text-base">{language.t("sidebar.gettingStarted.line1")}</div>
|
||||
<div class="text-text-base">{language.t("sidebar.gettingStarted.line2")}</div>
|
||||
</div>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-md rounded-t-none shadow-none border-t border-border-weak-base px-3"
|
||||
size="large"
|
||||
icon="plus"
|
||||
onClick={connectProvider}
|
||||
>
|
||||
{language.t("command.provider.connect")}
|
||||
</Button>
|
||||
|
||||
<div
|
||||
class="shrink-0 px-2 py-3 border-t border-border-weak-base"
|
||||
classList={{
|
||||
hidden: !(providers.all().length > 0 && providers.paid().length === 0),
|
||||
}}
|
||||
>
|
||||
<div class="rounded-md bg-background-base shadow-xs-border-base">
|
||||
<div class="p-3 flex flex-col gap-2">
|
||||
<div class="text-12-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div>
|
||||
<div class="text-text-base">{language.t("sidebar.gettingStarted.line1")}</div>
|
||||
<div class="text-text-base">{language.t("sidebar.gettingStarted.line2")}</div>
|
||||
</div>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-md rounded-t-none shadow-none border-t border-border-weak-base px-3"
|
||||
size="large"
|
||||
icon="plus"
|
||||
onClick={connectProvider}
|
||||
>
|
||||
{language.t("command.provider.connect")}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2816,7 +2833,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
return (
|
||||
<div class="flex h-full w-full overflow-hidden">
|
||||
<div class="w-16 shrink-0 bg-background-base flex flex-col items-center overflow-hidden">
|
||||
<div class="w-16 shrink-0 bg-background-base flex flex-col items-center overflow-hidden" onMouseMove={aim.move}>
|
||||
<div class="flex-1 min-h-0 w-full">
|
||||
<DragDropProvider
|
||||
onDragStart={handleDragStart}
|
||||
@@ -2911,6 +2928,7 @@ export default function Layout(props: ParentProps) {
|
||||
navLeave.current = undefined
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
aim.reset()
|
||||
if (!sidebarHovering()) return
|
||||
|
||||
if (navLeave.current !== undefined) clearTimeout(navLeave.current)
|
||||
@@ -2926,7 +2944,7 @@ export default function Layout(props: ParentProps) {
|
||||
</div>
|
||||
<Show when={!layout.sidebar.opened() ? hoverProjectData() : undefined} keyed>
|
||||
{(project) => (
|
||||
<div class="absolute inset-y-0 left-16 z-50 flex">
|
||||
<div class="absolute inset-y-0 left-16 z-50 flex" onMouseEnter={aim.reset}>
|
||||
<SidebarPanel project={project} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
+770
-118
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
||||
type Point = { x: number; y: number }
|
||||
|
||||
export function createAim(props: {
|
||||
enabled: () => boolean
|
||||
active: () => string | undefined
|
||||
el: () => HTMLElement | undefined
|
||||
onActivate: (id: string) => void
|
||||
delay?: number
|
||||
max?: number
|
||||
tolerance?: number
|
||||
edge?: number
|
||||
}) {
|
||||
const state = {
|
||||
locs: [] as Point[],
|
||||
timer: undefined as number | undefined,
|
||||
pending: undefined as string | undefined,
|
||||
over: undefined as string | undefined,
|
||||
last: undefined as Point | undefined,
|
||||
}
|
||||
|
||||
const delay = props.delay ?? 250
|
||||
const max = props.max ?? 4
|
||||
const tolerance = props.tolerance ?? 80
|
||||
const edge = props.edge ?? 18
|
||||
|
||||
const cancel = () => {
|
||||
if (state.timer !== undefined) clearTimeout(state.timer)
|
||||
state.timer = undefined
|
||||
state.pending = undefined
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
cancel()
|
||||
state.over = undefined
|
||||
state.last = undefined
|
||||
state.locs.length = 0
|
||||
}
|
||||
|
||||
const move = (event: MouseEvent) => {
|
||||
if (!props.enabled()) return
|
||||
const el = props.el()
|
||||
if (!el) return
|
||||
|
||||
const rect = el.getBoundingClientRect()
|
||||
const x = event.clientX
|
||||
const y = event.clientY
|
||||
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) return
|
||||
|
||||
state.locs.push({ x, y })
|
||||
if (state.locs.length > max) state.locs.shift()
|
||||
}
|
||||
|
||||
const wait = () => {
|
||||
if (!props.enabled()) return 0
|
||||
if (!props.active()) return 0
|
||||
|
||||
const el = props.el()
|
||||
if (!el) return 0
|
||||
if (state.locs.length < 2) return 0
|
||||
|
||||
const rect = el.getBoundingClientRect()
|
||||
const loc = state.locs[state.locs.length - 1]
|
||||
if (!loc) return 0
|
||||
|
||||
const prev = state.locs[0] ?? loc
|
||||
if (prev.x < rect.left || prev.x > rect.right || prev.y < rect.top || prev.y > rect.bottom) return 0
|
||||
if (state.last && loc.x === state.last.x && loc.y === state.last.y) return 0
|
||||
|
||||
if (rect.right - loc.x <= edge) {
|
||||
state.last = loc
|
||||
return delay
|
||||
}
|
||||
|
||||
const upper = { x: rect.right, y: rect.top - tolerance }
|
||||
const lower = { x: rect.right, y: rect.bottom + tolerance }
|
||||
const slope = (a: Point, b: Point) => (b.y - a.y) / (b.x - a.x)
|
||||
|
||||
const decreasing = slope(loc, upper)
|
||||
const increasing = slope(loc, lower)
|
||||
const prevDecreasing = slope(prev, upper)
|
||||
const prevIncreasing = slope(prev, lower)
|
||||
|
||||
if (decreasing < prevDecreasing && increasing > prevIncreasing) {
|
||||
state.last = loc
|
||||
return delay
|
||||
}
|
||||
|
||||
state.last = undefined
|
||||
return 0
|
||||
}
|
||||
|
||||
const activate = (id: string) => {
|
||||
cancel()
|
||||
props.onActivate(id)
|
||||
}
|
||||
|
||||
const request = (id: string) => {
|
||||
if (!id) return
|
||||
if (props.active() === id) return
|
||||
|
||||
if (!props.active()) {
|
||||
activate(id)
|
||||
return
|
||||
}
|
||||
|
||||
const ms = wait()
|
||||
if (ms === 0) {
|
||||
activate(id)
|
||||
return
|
||||
}
|
||||
|
||||
cancel()
|
||||
state.pending = id
|
||||
state.timer = window.setTimeout(() => {
|
||||
state.timer = undefined
|
||||
if (state.pending !== id) return
|
||||
state.pending = undefined
|
||||
if (!props.enabled()) return
|
||||
if (!props.active()) return
|
||||
if (state.over !== id) return
|
||||
props.onActivate(id)
|
||||
}, ms)
|
||||
}
|
||||
|
||||
const enter = (id: string, event: MouseEvent) => {
|
||||
if (!props.enabled()) return
|
||||
state.over = id
|
||||
move(event)
|
||||
request(id)
|
||||
}
|
||||
|
||||
const leave = (id: string) => {
|
||||
if (state.over === id) state.over = undefined
|
||||
if (state.pending === id) cancel()
|
||||
}
|
||||
|
||||
return { move, enter, leave, activate, request, cancel, reset }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function handler(
|
||||
const dataDumper = createDataDumper(sessionId, requestId, projectId)
|
||||
const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
|
||||
const isTrial = await trialLimiter?.isTrial()
|
||||
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip)
|
||||
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request.headers)
|
||||
await rateLimiter?.check()
|
||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
||||
const stickyProvider = await stickyTracker?.get()
|
||||
|
||||
@@ -26,6 +26,7 @@ export const oaCompatHelper: ProviderHelper = () => ({
|
||||
modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
headers.set("authorization", `Bearer ${apiKey}`)
|
||||
headers.set("x-session-affinity", headers.get("x-opencode-session") ?? "")
|
||||
},
|
||||
modifyBody: (body: Record<string, any>) => {
|
||||
return {
|
||||
|
||||
@@ -4,9 +4,11 @@ import { RateLimitError } from "./error"
|
||||
import { logger } from "./logger"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
|
||||
export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string) {
|
||||
export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, headers: Headers) {
|
||||
if (!limit) return
|
||||
|
||||
const limitValue = limit.checkHeader && !headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value
|
||||
|
||||
const ip = !rawIp.length ? "unknown" : rawIp
|
||||
const now = Date.now()
|
||||
const intervals =
|
||||
@@ -32,7 +34,7 @@ export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: s
|
||||
)
|
||||
const total = rows.reduce((sum, r) => sum + r.count, 0)
|
||||
logger.debug(`rate limit total: ${total}`)
|
||||
if (total >= limit.value) throw new RateLimitError(`Rate limit exceeded. Please try again later.`)
|
||||
if (total >= limitValue) throw new RateLimitError(`Rate limit exceeded. Please try again later.`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -25,9 +25,9 @@
|
||||
"db": "sst shell drizzle-kit",
|
||||
"db-dev": "sst shell --stage dev -- drizzle-kit",
|
||||
"db-prod": "sst shell --stage production -- drizzle-kit",
|
||||
"shell": "sst shell -- bun",
|
||||
"shell-dev": "sst shell --stage dev -- bun",
|
||||
"shell-prod": "sst shell --stage production -- bun",
|
||||
"shell": "sst shell",
|
||||
"shell-dev": "sst shell --stage dev",
|
||||
"shell-prod": "sst shell --stage production",
|
||||
"update-models": "script/update-models.ts",
|
||||
"promote-models-to-dev": "script/promote-models.ts dev",
|
||||
"promote-models-to-prod": "script/promote-models.ts production",
|
||||
|
||||
@@ -17,10 +17,8 @@ const oldValues = Array.from({ length: PARTS }, (_, i) => {
|
||||
?.split("=")
|
||||
.slice(1)
|
||||
.join("=")
|
||||
// TODO
|
||||
//if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
|
||||
//return value
|
||||
return value ?? ""
|
||||
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
|
||||
return value
|
||||
})
|
||||
|
||||
// store the prettified json to a temp file
|
||||
|
||||
@@ -21,6 +21,8 @@ export namespace ZenData {
|
||||
const RateLimitSchema = z.object({
|
||||
period: z.enum(["day", "rolling"]),
|
||||
value: z.number().int(),
|
||||
checkHeader: z.string().optional(),
|
||||
fallbackValue: z.number().int().optional(),
|
||||
})
|
||||
export type Format = z.infer<typeof FormatSchema>
|
||||
export type Trial = z.infer<typeof TrialSchema>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-dvh p-px"></div>
|
||||
<div id="root" class="flex flex-col h-dvh"></div>
|
||||
<div data-tauri-decorum-tb class="w-0 h-0 hidden" />
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -6,6 +6,19 @@
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
{
|
||||
"identifier": "opener:allow-open-path",
|
||||
"allow": [
|
||||
{ "path": "**/*" },
|
||||
{ "path": "/**/*" },
|
||||
{ "path": "**/.*/*/**" },
|
||||
{ "path": "/**/.*/*/**" },
|
||||
{ "path": "**/*", "app": true },
|
||||
{ "path": "/**/*", "app": true },
|
||||
{ "path": "**/.*/*/**", "app": true },
|
||||
{ "path": "/**/.*/*/**", "app": true }
|
||||
]
|
||||
},
|
||||
"deep-link:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-set-theme",
|
||||
|
||||
@@ -42,6 +42,13 @@
|
||||
"active": true,
|
||||
"targets": ["deb", "rpm", "dmg", "nsis", "app"],
|
||||
"externalBin": ["sidecars/opencode-cli"],
|
||||
"linux": {
|
||||
"rpm": {
|
||||
"compression": {
|
||||
"type": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
"macOS": {
|
||||
"entitlements": "./entitlements.plist"
|
||||
},
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
"files": {
|
||||
"/usr/share/metainfo/ai.opencode.opencode.metainfo.xml": "release/appstream.metainfo.xml"
|
||||
}
|
||||
},
|
||||
"rpm": {
|
||||
"compression": {
|
||||
"type": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
export const dict = {
|
||||
"desktop.menu.checkForUpdates": "Provjeri ažuriranja...",
|
||||
"desktop.menu.installCli": "Instaliraj CLI...",
|
||||
"desktop.menu.reloadWebview": "Ponovo učitavanje webview-a",
|
||||
"desktop.menu.restart": "Restartuj",
|
||||
|
||||
"desktop.dialog.chooseFolder": "Odaberi folder",
|
||||
"desktop.dialog.chooseFile": "Odaberi datoteku",
|
||||
"desktop.dialog.saveFile": "Sačuvaj datoteku",
|
||||
|
||||
"desktop.updater.checkFailed.title": "Provjera ažuriranja nije uspjela",
|
||||
"desktop.updater.checkFailed.message": "Nije moguće provjeriti ažuriranja",
|
||||
"desktop.updater.none.title": "Nema dostupnog ažuriranja",
|
||||
"desktop.updater.none.message": "Već koristiš najnoviju verziju OpenCode-a",
|
||||
"desktop.updater.downloadFailed.title": "Ažuriranje nije uspjelo",
|
||||
"desktop.updater.downloadFailed.message": "Neuspjelo preuzimanje ažuriranja",
|
||||
"desktop.updater.downloaded.title": "Ažuriranje preuzeto",
|
||||
"desktop.updater.downloaded.prompt":
|
||||
"Verzija {{version}} OpenCode-a je preuzeta. Želiš li da je instaliraš i ponovo pokreneš aplikaciju?",
|
||||
"desktop.updater.installFailed.title": "Ažuriranje nije uspjelo",
|
||||
"desktop.updater.installFailed.message": "Neuspjela instalacija ažuriranja",
|
||||
|
||||
"desktop.cli.installed.title": "CLI instaliran",
|
||||
"desktop.cli.installed.message":
|
||||
"CLI je instaliran u {{path}}\n\nRestartuj terminal da bi koristio komandu 'opencode'.",
|
||||
"desktop.cli.failed.title": "Instalacija nije uspjela",
|
||||
"desktop.cli.failed.message": "Neuspjela instalacija CLI-a: {{error}}",
|
||||
|
||||
"desktop.error.serverStartFailed.title": "OpenCode se nije mogao pokrenuti",
|
||||
"desktop.error.serverStartFailed.description":
|
||||
"Lokalni OpenCode server se nije mogao pokrenuti. Restartuj aplikaciju ili provjeri mrežne postavke (VPN/proxy) i pokušaj ponovo.",
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { dict as desktopRu } from "./ru"
|
||||
import { dict as desktopAr } from "./ar"
|
||||
import { dict as desktopNo } from "./no"
|
||||
import { dict as desktopBr } from "./br"
|
||||
import { dict as desktopBs } from "./bs"
|
||||
|
||||
import { dict as appEn } from "../../../app/src/i18n/en"
|
||||
import { dict as appZh } from "../../../app/src/i18n/zh"
|
||||
@@ -30,13 +31,45 @@ import { dict as appRu } from "../../../app/src/i18n/ru"
|
||||
import { dict as appAr } from "../../../app/src/i18n/ar"
|
||||
import { dict as appNo } from "../../../app/src/i18n/no"
|
||||
import { dict as appBr } from "../../../app/src/i18n/br"
|
||||
import { dict as appBs } from "../../../app/src/i18n/bs"
|
||||
|
||||
export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br"
|
||||
export type Locale =
|
||||
| "en"
|
||||
| "zh"
|
||||
| "zht"
|
||||
| "ko"
|
||||
| "de"
|
||||
| "es"
|
||||
| "fr"
|
||||
| "da"
|
||||
| "ja"
|
||||
| "pl"
|
||||
| "ru"
|
||||
| "ar"
|
||||
| "no"
|
||||
| "br"
|
||||
| "bs"
|
||||
|
||||
type RawDictionary = typeof appEn & typeof desktopEn
|
||||
type Dictionary = i18n.Flatten<RawDictionary>
|
||||
|
||||
const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"]
|
||||
const LOCALES: readonly Locale[] = [
|
||||
"en",
|
||||
"zh",
|
||||
"zht",
|
||||
"ko",
|
||||
"de",
|
||||
"es",
|
||||
"fr",
|
||||
"da",
|
||||
"ja",
|
||||
"pl",
|
||||
"ru",
|
||||
"bs",
|
||||
"ar",
|
||||
"no",
|
||||
"br",
|
||||
]
|
||||
|
||||
function detectLocale(): Locale {
|
||||
if (typeof navigator !== "object") return "en"
|
||||
@@ -64,6 +97,7 @@ function detectLocale(): Locale {
|
||||
)
|
||||
return "no"
|
||||
if (language.toLowerCase().startsWith("pt")) return "br"
|
||||
if (language.toLowerCase().startsWith("bs")) return "bs"
|
||||
}
|
||||
|
||||
return "en"
|
||||
@@ -108,6 +142,7 @@ function build(locale: Locale): Dictionary {
|
||||
if (locale === "ar") return { ...base, ...i18n.flatten(appAr), ...i18n.flatten(desktopAr) }
|
||||
if (locale === "no") return { ...base, ...i18n.flatten(appNo), ...i18n.flatten(desktopNo) }
|
||||
if (locale === "br") return { ...base, ...i18n.flatten(appBr), ...i18n.flatten(desktopBr) }
|
||||
if (locale === "bs") return { ...base, ...i18n.flatten(appBs), ...i18n.flatten(desktopBs) }
|
||||
return { ...base, ...i18n.flatten(appKo), ...i18n.flatten(desktopKo) }
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { render } from "solid-js/web"
|
||||
import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app"
|
||||
import { open, save } from "@tauri-apps/plugin-dialog"
|
||||
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
|
||||
import { openPath as openerOpenPath } from "@tauri-apps/plugin-opener"
|
||||
import { open as shellOpen } from "@tauri-apps/plugin-shell"
|
||||
import { type as ostype } from "@tauri-apps/plugin-os"
|
||||
import { check, Update } from "@tauri-apps/plugin-updater"
|
||||
@@ -30,17 +31,6 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
|
||||
void initI18n()
|
||||
|
||||
// Floating UI can call getComputedStyle with non-elements (e.g., null refs, virtual elements).
|
||||
// This happens on all platforms (WebView2 on Windows, WKWebView on macOS), not just Windows.
|
||||
const originalGetComputedStyle = window.getComputedStyle
|
||||
window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => {
|
||||
if (!(elt instanceof Element)) {
|
||||
// Fall back to a safe element when a non-element is passed.
|
||||
return originalGetComputedStyle(document.documentElement, pseudoElt ?? undefined)
|
||||
}
|
||||
return originalGetComputedStyle(elt, pseudoElt ?? undefined)
|
||||
}) as typeof window.getComputedStyle
|
||||
|
||||
let update: Update | null = null
|
||||
|
||||
const deepLinkEvent = "opencode:deep-link"
|
||||
@@ -98,6 +88,10 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
|
||||
void shellOpen(url).catch(() => undefined)
|
||||
},
|
||||
|
||||
openPath(path: string, app?: string) {
|
||||
return openerOpenPath(path, app)
|
||||
},
|
||||
|
||||
back() {
|
||||
window.history.back()
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.1.48"
|
||||
version = "1.1.52"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.48/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.52/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.48/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.52/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.48/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.52/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.48/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.52/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.48/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.52/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.1.48",
|
||||
"version": "1.1.52",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -20,6 +20,7 @@
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode"
|
||||
},
|
||||
"randomField": "this-is-a-random-value-12345",
|
||||
"exports": {
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
@@ -49,7 +50,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.13.0",
|
||||
"@agentclientprotocol/sdk": "0.14.1",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.74",
|
||||
"@ai-sdk/anthropic": "2.0.58",
|
||||
"@ai-sdk/azure": "2.0.91",
|
||||
@@ -68,9 +69,10 @@
|
||||
"@ai-sdk/provider-utils": "3.0.20",
|
||||
"@ai-sdk/togetherai": "1.0.34",
|
||||
"@ai-sdk/vercel": "1.0.33",
|
||||
"@ai-sdk/xai": "2.0.56",
|
||||
"@ai-sdk/xai": "2.0.51",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@gitlab/gitlab-ai-provider": "3.3.1",
|
||||
"@gitlab/gitlab-ai-provider": "3.4.0",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.2",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.25.2",
|
||||
@@ -91,8 +93,9 @@
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"ai": "catalog:",
|
||||
"ai-gateway-provider": "2.3.1",
|
||||
"bonjour-service": "1.3.0",
|
||||
"bun-pty": "0.4.4",
|
||||
"bun-pty": "0.4.8",
|
||||
"chokidar": "4.0.3",
|
||||
"clipboardy": "4.0.0",
|
||||
"decimal.js": "10.5.0",
|
||||
|
||||
@@ -18,6 +18,7 @@ const version = Object.values(binaries)[0]
|
||||
await $`mkdir -p ./dist/${pkg.name}`
|
||||
await $`cp -r ./bin ./dist/${pkg.name}/bin`
|
||||
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
|
||||
await Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text())
|
||||
|
||||
await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
@@ -30,6 +31,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
|
||||
},
|
||||
version: version,
|
||||
license: pkg.license,
|
||||
optionalDependencies: binaries,
|
||||
},
|
||||
null,
|
||||
@@ -93,73 +95,7 @@ if (!Script.preview) {
|
||||
"",
|
||||
].join("\n")
|
||||
|
||||
// Source-based PKGBUILD for opencode
|
||||
const sourcePkgbuild = [
|
||||
"# Maintainer: dax",
|
||||
"# Maintainer: adam",
|
||||
"",
|
||||
"pkgname='opencode'",
|
||||
`pkgver=${pkgver}`,
|
||||
`_subver=${_subver}`,
|
||||
"options=('!debug' '!strip')",
|
||||
"pkgrel=1",
|
||||
"pkgdesc='The AI coding agent built for the terminal.'",
|
||||
"url='https://github.com/anomalyco/opencode'",
|
||||
"arch=('aarch64' 'x86_64')",
|
||||
"license=('MIT')",
|
||||
"provides=('opencode')",
|
||||
"conflicts=('opencode-bin')",
|
||||
"depends=('ripgrep')",
|
||||
"makedepends=('git' 'bun' 'go')",
|
||||
"",
|
||||
`source=("opencode-\${pkgver}.tar.gz::https://github.com/anomalyco/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
|
||||
`sha256sums=('SKIP')`,
|
||||
"",
|
||||
"build() {",
|
||||
` cd "opencode-\${pkgver}"`,
|
||||
` bun install`,
|
||||
" cd ./packages/opencode",
|
||||
` OPENCODE_CHANNEL=latest OPENCODE_VERSION=${pkgver} bun run ./script/build.ts --single`,
|
||||
"}",
|
||||
"",
|
||||
"package() {",
|
||||
` cd "opencode-\${pkgver}/packages/opencode"`,
|
||||
' mkdir -p "${pkgdir}/usr/bin"',
|
||||
' target_arch="x64"',
|
||||
' case "$CARCH" in',
|
||||
' x86_64) target_arch="x64" ;;',
|
||||
' aarch64) target_arch="arm64" ;;',
|
||||
' *) printf "unsupported architecture: %s\\n" "$CARCH" >&2 ; return 1 ;;',
|
||||
" esac",
|
||||
' libc=""',
|
||||
" if command -v ldd >/dev/null 2>&1; then",
|
||||
" if ldd --version 2>&1 | grep -qi musl; then",
|
||||
' libc="-musl"',
|
||||
" fi",
|
||||
" fi",
|
||||
' if [ -z "$libc" ] && ls /lib/ld-musl-* >/dev/null 2>&1; then',
|
||||
' libc="-musl"',
|
||||
" fi",
|
||||
' base=""',
|
||||
' if [ "$target_arch" = "x64" ]; then',
|
||||
" if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then",
|
||||
' base="-baseline"',
|
||||
" fi",
|
||||
" fi",
|
||||
' bin="dist/opencode-linux-${target_arch}${base}${libc}/bin/opencode"',
|
||||
' if [ ! -f "$bin" ]; then',
|
||||
' printf "unable to find binary for %s%s%s\\n" "$target_arch" "$base" "$libc" >&2',
|
||||
" return 1",
|
||||
" fi",
|
||||
' install -Dm755 "$bin" "${pkgdir}/usr/bin/opencode"',
|
||||
"}",
|
||||
"",
|
||||
].join("\n")
|
||||
|
||||
for (const [pkg, pkgbuild] of [
|
||||
["opencode-bin", binaryPkgbuild],
|
||||
["opencode", sourcePkgbuild],
|
||||
]) {
|
||||
for (const [pkg, pkgbuild] of [["opencode-bin", binaryPkgbuild]]) {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
try {
|
||||
await $`rm -rf ./dist/aur-${pkg}`
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type SetSessionModeResponse,
|
||||
type ToolCallContent,
|
||||
type ToolKind,
|
||||
type Usage,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
|
||||
import { Log } from "../util/log"
|
||||
@@ -38,7 +39,7 @@ import { Config } from "@/config/config"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { z } from "zod"
|
||||
import { LoadAPIKeyError } from "ai"
|
||||
import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
|
||||
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
|
||||
import { applyPatch } from "diff"
|
||||
|
||||
type ModeOption = { id: string; name: string; description?: string }
|
||||
@@ -49,6 +50,74 @@ const DEFAULT_VARIANT_VALUE = "default"
|
||||
export namespace ACP {
|
||||
const log = Log.create({ service: "acp-agent" })
|
||||
|
||||
async function getContextLimit(
|
||||
sdk: OpencodeClient,
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
directory: string,
|
||||
): Promise<number | null> {
|
||||
const providers = await sdk.config
|
||||
.providers({ directory })
|
||||
.then((x) => x.data?.providers ?? [])
|
||||
.catch((error) => {
|
||||
log.error("failed to get providers for context limit", { error })
|
||||
return []
|
||||
})
|
||||
|
||||
const provider = providers.find((p) => p.id === providerID)
|
||||
const model = provider?.models[modelID]
|
||||
return model?.limit.context ?? null
|
||||
}
|
||||
|
||||
async function sendUsageUpdate(
|
||||
connection: AgentSideConnection,
|
||||
sdk: OpencodeClient,
|
||||
sessionID: string,
|
||||
directory: string,
|
||||
): Promise<void> {
|
||||
const messages = await sdk.session
|
||||
.messages({ sessionID, directory }, { throwOnError: true })
|
||||
.then((x) => x.data)
|
||||
.catch((error) => {
|
||||
log.error("failed to fetch messages for usage update", { error })
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!messages) return
|
||||
|
||||
const assistantMessages = messages.filter(
|
||||
(m): m is { info: AssistantMessage; parts: SessionMessageResponse["parts"] } => m.info.role === "assistant",
|
||||
)
|
||||
|
||||
const lastAssistant = assistantMessages[assistantMessages.length - 1]
|
||||
if (!lastAssistant) return
|
||||
|
||||
const msg = lastAssistant.info
|
||||
const size = await getContextLimit(sdk, msg.providerID, msg.modelID, directory)
|
||||
|
||||
if (!size) {
|
||||
// Cannot calculate usage without known context size
|
||||
return
|
||||
}
|
||||
|
||||
const used = msg.tokens.input + (msg.tokens.cache?.read ?? 0)
|
||||
const totalCost = assistantMessages.reduce((sum, m) => sum + m.info.cost, 0)
|
||||
|
||||
await connection
|
||||
.sessionUpdate({
|
||||
sessionId: sessionID,
|
||||
update: {
|
||||
sessionUpdate: "usage_update",
|
||||
used,
|
||||
size,
|
||||
cost: { amount: totalCost, currency: "USD" },
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send usage update", { error })
|
||||
})
|
||||
}
|
||||
|
||||
export async function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
|
||||
return {
|
||||
create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
|
||||
@@ -546,6 +615,8 @@ export namespace ACP {
|
||||
await this.processMessage(msg)
|
||||
}
|
||||
|
||||
await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
|
||||
|
||||
return result
|
||||
} catch (e) {
|
||||
const error = MessageV2.fromError(e, {
|
||||
@@ -654,6 +725,8 @@ export namespace ACP {
|
||||
await this.processMessage(msg)
|
||||
}
|
||||
|
||||
await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
|
||||
|
||||
return mode
|
||||
} catch (e) {
|
||||
const error = MessageV2.fromError(e, {
|
||||
@@ -677,11 +750,15 @@ export namespace ACP {
|
||||
|
||||
log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
|
||||
|
||||
return this.loadSessionMode({
|
||||
const result = await this.loadSessionMode({
|
||||
cwd: directory,
|
||||
mcpServers,
|
||||
sessionId,
|
||||
})
|
||||
|
||||
await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
|
||||
|
||||
return result
|
||||
} catch (e) {
|
||||
const error = MessageV2.fromError(e, {
|
||||
providerID: this.config.defaultModel?.providerID ?? "unknown",
|
||||
@@ -1239,13 +1316,22 @@ export namespace ACP {
|
||||
return { name, args: rest.join(" ").trim() }
|
||||
})()
|
||||
|
||||
const done = {
|
||||
stopReason: "end_turn" as const,
|
||||
_meta: {},
|
||||
}
|
||||
const buildUsage = (msg: AssistantMessage): Usage => ({
|
||||
totalTokens:
|
||||
msg.tokens.input +
|
||||
msg.tokens.output +
|
||||
msg.tokens.reasoning +
|
||||
(msg.tokens.cache?.read ?? 0) +
|
||||
(msg.tokens.cache?.write ?? 0),
|
||||
inputTokens: msg.tokens.input,
|
||||
outputTokens: msg.tokens.output,
|
||||
thoughtTokens: msg.tokens.reasoning || undefined,
|
||||
cachedReadTokens: msg.tokens.cache?.read || undefined,
|
||||
cachedWriteTokens: msg.tokens.cache?.write || undefined,
|
||||
})
|
||||
|
||||
if (!cmd) {
|
||||
await this.sdk.session.prompt({
|
||||
const response = await this.sdk.session.prompt({
|
||||
sessionID,
|
||||
model: {
|
||||
providerID: model.providerID,
|
||||
@@ -1256,14 +1342,22 @@ export namespace ACP {
|
||||
agent,
|
||||
directory,
|
||||
})
|
||||
return done
|
||||
const msg = response.data?.info
|
||||
|
||||
await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
|
||||
|
||||
return {
|
||||
stopReason: "end_turn" as const,
|
||||
usage: msg ? buildUsage(msg) : undefined,
|
||||
_meta: {},
|
||||
}
|
||||
}
|
||||
|
||||
const command = await this.config.sdk.command
|
||||
.list({ directory }, { throwOnError: true })
|
||||
.then((x) => x.data!.find((c) => c.name === cmd.name))
|
||||
if (command) {
|
||||
await this.sdk.session.command({
|
||||
const response = await this.sdk.session.command({
|
||||
sessionID,
|
||||
command: command.name,
|
||||
arguments: cmd.args,
|
||||
@@ -1271,7 +1365,15 @@ export namespace ACP {
|
||||
agent,
|
||||
directory,
|
||||
})
|
||||
return done
|
||||
const msg = response.data?.info
|
||||
|
||||
await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
|
||||
|
||||
return {
|
||||
stopReason: "end_turn" as const,
|
||||
usage: msg ? buildUsage(msg) : undefined,
|
||||
_meta: {},
|
||||
}
|
||||
}
|
||||
|
||||
switch (cmd.name) {
|
||||
@@ -1288,7 +1390,12 @@ export namespace ACP {
|
||||
break
|
||||
}
|
||||
|
||||
return done
|
||||
await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
|
||||
|
||||
return {
|
||||
stopReason: "end_turn" as const,
|
||||
_meta: {},
|
||||
}
|
||||
}
|
||||
|
||||
async cancel(params: CancelNotification) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { mergeDeep, pipe, sortBy, values } from "remeda"
|
||||
import { Global } from "@/global"
|
||||
import path from "path"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Skill } from "../skill"
|
||||
|
||||
export namespace Agent {
|
||||
export const Info = z
|
||||
@@ -50,13 +51,14 @@ export namespace Agent {
|
||||
const state = Instance.state(async () => {
|
||||
const cfg = await Config.get()
|
||||
|
||||
const skillDirs = await Skill.dirs()
|
||||
const defaults = PermissionNext.fromConfig({
|
||||
"*": "allow",
|
||||
doom_loop: "ask",
|
||||
external_directory: {
|
||||
"*": "ask",
|
||||
[Truncate.DIR]: "allow",
|
||||
[Truncate.GLOB]: "allow",
|
||||
...Object.fromEntries(skillDirs.map((dir) => [path.join(dir, "*"), "allow"])),
|
||||
},
|
||||
question: "deny",
|
||||
plan_enter: "deny",
|
||||
@@ -140,7 +142,6 @@ export namespace Agent {
|
||||
codesearch: "allow",
|
||||
read: "allow",
|
||||
external_directory: {
|
||||
[Truncate.DIR]: "allow",
|
||||
[Truncate.GLOB]: "allow",
|
||||
},
|
||||
}),
|
||||
@@ -229,19 +230,19 @@ export namespace Agent {
|
||||
item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}))
|
||||
}
|
||||
|
||||
// Ensure Truncate.DIR is allowed unless explicitly configured
|
||||
// Ensure Truncate.GLOB is allowed unless explicitly configured
|
||||
for (const name in result) {
|
||||
const agent = result[name]
|
||||
const explicit = agent.permission.some((r) => {
|
||||
if (r.permission !== "external_directory") return false
|
||||
if (r.action !== "deny") return false
|
||||
return r.pattern === Truncate.DIR || r.pattern === Truncate.GLOB
|
||||
return r.pattern === Truncate.GLOB
|
||||
})
|
||||
if (explicit) continue
|
||||
|
||||
result[name].permission = PermissionNext.merge(
|
||||
result[name].permission,
|
||||
PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow" } }),
|
||||
PermissionNext.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import path from "path"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { readableStreamToText } from "bun"
|
||||
import { createRequire } from "module"
|
||||
import { Lock } from "../util/lock"
|
||||
import { PackageRegistry } from "./registry"
|
||||
import { proxied } from "@/util/proxied"
|
||||
|
||||
export namespace BunProc {
|
||||
const log = Log.create({ service: "bun" })
|
||||
const req = createRequire(import.meta.url)
|
||||
|
||||
export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
|
||||
log.info("running", {
|
||||
@@ -75,14 +75,17 @@ export namespace BunProc {
|
||||
const dependencies = parsed.dependencies ?? {}
|
||||
if (!parsed.dependencies) parsed.dependencies = dependencies
|
||||
const modExists = await Filesystem.exists(mod)
|
||||
if (dependencies[pkg] === version && modExists) return mod
|
||||
const cachedVersion = dependencies[pkg]
|
||||
|
||||
const proxied = !!(
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.https_proxy
|
||||
)
|
||||
if (!modExists || !cachedVersion) {
|
||||
// continue to install
|
||||
} else if (version !== "latest" && cachedVersion === version) {
|
||||
return mod
|
||||
} else if (version === "latest") {
|
||||
const isOutdated = await PackageRegistry.isOutdated(pkg, cachedVersion, Global.Path.cache)
|
||||
if (!isOutdated) return mod
|
||||
log.info("Cached version is outdated, proceeding with install", { pkg, cachedVersion })
|
||||
}
|
||||
|
||||
// Build command arguments
|
||||
const args = [
|
||||
@@ -90,7 +93,7 @@ export namespace BunProc {
|
||||
"--force",
|
||||
"--exact",
|
||||
// TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
|
||||
...(proxied ? ["--no-cache"] : []),
|
||||
...(proxied() ? ["--no-cache"] : []),
|
||||
"--cwd",
|
||||
Global.Path.cache,
|
||||
pkg + "@" + version,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { readableStreamToText, semver } from "bun"
|
||||
import { Log } from "../util/log"
|
||||
|
||||
export namespace PackageRegistry {
|
||||
const log = Log.create({ service: "bun" })
|
||||
|
||||
function which() {
|
||||
return process.execPath
|
||||
}
|
||||
|
||||
export async function info(pkg: string, field: string, cwd?: string): Promise<string | null> {
|
||||
const result = Bun.spawn([which(), "info", pkg, field], {
|
||||
cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
})
|
||||
|
||||
const code = await result.exited
|
||||
const stdout = result.stdout ? await readableStreamToText(result.stdout) : ""
|
||||
const stderr = result.stderr ? await readableStreamToText(result.stderr) : ""
|
||||
|
||||
if (code !== 0) {
|
||||
log.warn("bun info failed", { pkg, field, code, stderr })
|
||||
return null
|
||||
}
|
||||
|
||||
const value = stdout.trim()
|
||||
if (!value) return null
|
||||
return value
|
||||
}
|
||||
|
||||
export async function isOutdated(pkg: string, cachedVersion: string, cwd?: string): Promise<boolean> {
|
||||
const latestVersion = await info(pkg, "version", cwd)
|
||||
if (!latestVersion) {
|
||||
log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion })
|
||||
return false
|
||||
}
|
||||
|
||||
const isRange = /[\s^~*xX<>|=]/.test(cachedVersion)
|
||||
if (isRange) return !semver.satisfies(latestVersion, cachedVersion)
|
||||
|
||||
return semver.order(cachedVersion, latestVersion) === -1
|
||||
}
|
||||
}
|
||||
@@ -4,25 +4,211 @@ import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { Command } from "../../command"
|
||||
import { EOL } from "os"
|
||||
import { select } from "@clack/prompts"
|
||||
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import { Server } from "../../server/server"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { Agent } from "../../agent/agent"
|
||||
import { PermissionNext } from "../../permission/next"
|
||||
import { Tool } from "../../tool/tool"
|
||||
import { GlobTool } from "../../tool/glob"
|
||||
import { GrepTool } from "../../tool/grep"
|
||||
import { ListTool } from "../../tool/ls"
|
||||
import { ReadTool } from "../../tool/read"
|
||||
import { WebFetchTool } from "../../tool/webfetch"
|
||||
import { EditTool } from "../../tool/edit"
|
||||
import { WriteTool } from "../../tool/write"
|
||||
import { CodeSearchTool } from "../../tool/codesearch"
|
||||
import { WebSearchTool } from "../../tool/websearch"
|
||||
import { TaskTool } from "../../tool/task"
|
||||
import { SkillTool } from "../../tool/skill"
|
||||
import { BashTool } from "../../tool/bash"
|
||||
import { TodoWriteTool } from "../../tool/todo"
|
||||
import { Locale } from "../../util/locale"
|
||||
|
||||
const TOOL: Record<string, [string, string]> = {
|
||||
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
|
||||
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
|
||||
glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
|
||||
grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
|
||||
list: ["List", UI.Style.TEXT_INFO_BOLD],
|
||||
read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
|
||||
write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
|
||||
websearch: ["Search", UI.Style.TEXT_DIM_BOLD],
|
||||
type ToolProps<T extends Tool.Info> = {
|
||||
input: Tool.InferParameters<T>
|
||||
metadata: Tool.InferMetadata<T>
|
||||
part: ToolPart
|
||||
}
|
||||
|
||||
function props<T extends Tool.Info>(part: ToolPart): ToolProps<T> {
|
||||
const state = part.state
|
||||
return {
|
||||
input: state.input as Tool.InferParameters<T>,
|
||||
metadata: ("metadata" in state ? state.metadata : {}) as Tool.InferMetadata<T>,
|
||||
part,
|
||||
}
|
||||
}
|
||||
|
||||
type Inline = {
|
||||
icon: string
|
||||
title: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
function inline(info: Inline) {
|
||||
const suffix = info.description ? UI.Style.TEXT_DIM + ` ${info.description}` + UI.Style.TEXT_NORMAL : ""
|
||||
UI.println(UI.Style.TEXT_NORMAL + info.icon, UI.Style.TEXT_NORMAL + info.title + suffix)
|
||||
}
|
||||
|
||||
function block(info: Inline, output?: string) {
|
||||
UI.empty()
|
||||
inline(info)
|
||||
if (!output?.trim()) return
|
||||
UI.println(output)
|
||||
UI.empty()
|
||||
}
|
||||
|
||||
function fallback(part: ToolPart) {
|
||||
const state = part.state
|
||||
const input = "input" in state ? state.input : undefined
|
||||
const title =
|
||||
("title" in state && state.title ? state.title : undefined) ||
|
||||
(input && typeof input === "object" && Object.keys(input).length > 0 ? JSON.stringify(input) : "Unknown")
|
||||
inline({
|
||||
icon: "⚙",
|
||||
title: `${part.tool} ${title}`,
|
||||
})
|
||||
}
|
||||
|
||||
function glob(info: ToolProps<typeof GlobTool>) {
|
||||
const root = info.input.path ?? ""
|
||||
const title = `Glob "${info.input.pattern}"`
|
||||
const suffix = root ? `in ${normalizePath(root)}` : ""
|
||||
const num = info.metadata.count
|
||||
const description =
|
||||
num === undefined ? suffix : `${suffix}${suffix ? " · " : ""}${num} ${num === 1 ? "match" : "matches"}`
|
||||
inline({
|
||||
icon: "✱",
|
||||
title,
|
||||
...(description && { description }),
|
||||
})
|
||||
}
|
||||
|
||||
function grep(info: ToolProps<typeof GrepTool>) {
|
||||
const root = info.input.path ?? ""
|
||||
const title = `Grep "${info.input.pattern}"`
|
||||
const suffix = root ? `in ${normalizePath(root)}` : ""
|
||||
const num = info.metadata.matches
|
||||
const description =
|
||||
num === undefined ? suffix : `${suffix}${suffix ? " · " : ""}${num} ${num === 1 ? "match" : "matches"}`
|
||||
inline({
|
||||
icon: "✱",
|
||||
title,
|
||||
...(description && { description }),
|
||||
})
|
||||
}
|
||||
|
||||
function list(info: ToolProps<typeof ListTool>) {
|
||||
const dir = info.input.path ? normalizePath(info.input.path) : ""
|
||||
inline({
|
||||
icon: "→",
|
||||
title: dir ? `List ${dir}` : "List",
|
||||
})
|
||||
}
|
||||
|
||||
function read(info: ToolProps<typeof ReadTool>) {
|
||||
const file = normalizePath(info.input.filePath)
|
||||
const pairs = Object.entries(info.input).filter(([key, value]) => {
|
||||
if (key === "filePath") return false
|
||||
return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
||||
})
|
||||
const description = pairs.length ? `[${pairs.map(([key, value]) => `${key}=${value}`).join(", ")}]` : undefined
|
||||
inline({
|
||||
icon: "→",
|
||||
title: `Read ${file}`,
|
||||
...(description && { description }),
|
||||
})
|
||||
}
|
||||
|
||||
function write(info: ToolProps<typeof WriteTool>) {
|
||||
block(
|
||||
{
|
||||
icon: "←",
|
||||
title: `Write ${normalizePath(info.input.filePath)}`,
|
||||
},
|
||||
info.part.state.status === "completed" ? info.part.state.output : undefined,
|
||||
)
|
||||
}
|
||||
|
||||
function webfetch(info: ToolProps<typeof WebFetchTool>) {
|
||||
inline({
|
||||
icon: "%",
|
||||
title: `WebFetch ${info.input.url}`,
|
||||
})
|
||||
}
|
||||
|
||||
function edit(info: ToolProps<typeof EditTool>) {
|
||||
const title = normalizePath(info.input.filePath)
|
||||
const diff = info.metadata.diff
|
||||
block(
|
||||
{
|
||||
icon: "←",
|
||||
title: `Edit ${title}`,
|
||||
},
|
||||
diff,
|
||||
)
|
||||
}
|
||||
|
||||
function codesearch(info: ToolProps<typeof CodeSearchTool>) {
|
||||
inline({
|
||||
icon: "◇",
|
||||
title: `Exa Code Search "${info.input.query}"`,
|
||||
})
|
||||
}
|
||||
|
||||
function websearch(info: ToolProps<typeof WebSearchTool>) {
|
||||
inline({
|
||||
icon: "◈",
|
||||
title: `Exa Web Search "${info.input.query}"`,
|
||||
})
|
||||
}
|
||||
|
||||
function task(info: ToolProps<typeof TaskTool>) {
|
||||
const agent = Locale.titlecase(info.input.subagent_type)
|
||||
const desc = info.input.description
|
||||
const started = info.part.state.status === "running"
|
||||
const name = desc ?? `${agent} Task`
|
||||
inline({
|
||||
icon: started ? "•" : "✓",
|
||||
title: name,
|
||||
description: desc ? `${agent} Agent` : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
function skill(info: ToolProps<typeof SkillTool>) {
|
||||
inline({
|
||||
icon: "→",
|
||||
title: `Skill "${info.input.name}"`,
|
||||
})
|
||||
}
|
||||
|
||||
function bash(info: ToolProps<typeof BashTool>) {
|
||||
const output = info.part.state.status === "completed" ? info.part.state.output?.trim() : undefined
|
||||
block(
|
||||
{
|
||||
icon: "$",
|
||||
title: `${info.input.command}`,
|
||||
},
|
||||
output,
|
||||
)
|
||||
}
|
||||
|
||||
function todo(info: ToolProps<typeof TodoWriteTool>) {
|
||||
block(
|
||||
{
|
||||
icon: "#",
|
||||
title: "Todos",
|
||||
},
|
||||
info.input.todos.map((item) => `${item.status === "completed" ? "[x]" : "[ ]"} ${item.content}`).join("\n"),
|
||||
)
|
||||
}
|
||||
|
||||
function normalizePath(input?: string) {
|
||||
if (!input) return ""
|
||||
if (path.isAbsolute(input)) return path.relative(process.cwd(), input) || "."
|
||||
return input
|
||||
}
|
||||
|
||||
export const RunCommand = cmd({
|
||||
@@ -91,17 +277,22 @@ export const RunCommand = cmd({
|
||||
type: "string",
|
||||
describe: "model variant (provider-specific reasoning effort, e.g., high, max, minimal)",
|
||||
})
|
||||
.option("thinking", {
|
||||
type: "boolean",
|
||||
describe: "show thinking blocks",
|
||||
default: false,
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
let message = [...args.message, ...(args["--"] || [])]
|
||||
.map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
|
||||
.join(" ")
|
||||
|
||||
const fileParts: any[] = []
|
||||
const files: { type: "file"; url: string; filename: string; mime: string }[] = []
|
||||
if (args.file) {
|
||||
const files = Array.isArray(args.file) ? args.file : [args.file]
|
||||
const list = Array.isArray(args.file) ? args.file : [args.file]
|
||||
|
||||
for (const filePath of files) {
|
||||
for (const filePath of list) {
|
||||
const resolvedPath = path.resolve(process.cwd(), filePath)
|
||||
const file = Bun.file(resolvedPath)
|
||||
const stats = await file.stat().catch(() => {})
|
||||
@@ -117,7 +308,7 @@ export const RunCommand = cmd({
|
||||
const stat = await file.stat()
|
||||
const mime = stat.isDirectory() ? "application/x-directory" : "text/plain"
|
||||
|
||||
fileParts.push({
|
||||
files.push({
|
||||
type: "file",
|
||||
url: `file://${resolvedPath}`,
|
||||
filename: path.basename(resolvedPath),
|
||||
@@ -133,17 +324,75 @@ export const RunCommand = cmd({
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const execute = async (sdk: OpencodeClient, sessionID: string) => {
|
||||
const printEvent = (color: string, type: string, title: string) => {
|
||||
UI.println(
|
||||
color + `|`,
|
||||
UI.Style.TEXT_NORMAL + UI.Style.TEXT_DIM + ` ${type.padEnd(7, " ")}`,
|
||||
"",
|
||||
UI.Style.TEXT_NORMAL + title,
|
||||
)
|
||||
const rules: PermissionNext.Ruleset = [
|
||||
{
|
||||
permission: "question",
|
||||
action: "deny",
|
||||
pattern: "*",
|
||||
},
|
||||
{
|
||||
permission: "plan_enter",
|
||||
action: "deny",
|
||||
pattern: "*",
|
||||
},
|
||||
{
|
||||
permission: "plan_exit",
|
||||
action: "deny",
|
||||
pattern: "*",
|
||||
},
|
||||
]
|
||||
|
||||
function title() {
|
||||
if (args.title === undefined) return
|
||||
if (args.title !== "") return args.title
|
||||
return message.slice(0, 50) + (message.length > 50 ? "..." : "")
|
||||
}
|
||||
|
||||
async function session(sdk: OpencodeClient) {
|
||||
if (args.continue) {
|
||||
const result = await sdk.session.list()
|
||||
return result.data?.find((s) => !s.parentID)?.id
|
||||
}
|
||||
if (args.session) return args.session
|
||||
const name = title()
|
||||
const result = await sdk.session.create({ title: name, permission: rules })
|
||||
return result.data?.id
|
||||
}
|
||||
|
||||
async function share(sdk: OpencodeClient, sessionID: string) {
|
||||
const cfg = await sdk.config.get()
|
||||
if (!cfg.data) return
|
||||
if (cfg.data.share !== "auto" && !Flag.OPENCODE_AUTO_SHARE && !args.share) return
|
||||
const res = await sdk.session.share({ sessionID }).catch((error) => {
|
||||
if (error instanceof Error && error.message.includes("disabled")) {
|
||||
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
|
||||
}
|
||||
return { error }
|
||||
})
|
||||
if (!res.error && "data" in res && res.data?.share?.url) {
|
||||
UI.println(UI.Style.TEXT_INFO_BOLD + "~ " + res.data.share.url)
|
||||
}
|
||||
}
|
||||
|
||||
async function execute(sdk: OpencodeClient) {
|
||||
function tool(part: ToolPart) {
|
||||
if (part.tool === "bash") return bash(props<typeof BashTool>(part))
|
||||
if (part.tool === "glob") return glob(props<typeof GlobTool>(part))
|
||||
if (part.tool === "grep") return grep(props<typeof GrepTool>(part))
|
||||
if (part.tool === "list") return list(props<typeof ListTool>(part))
|
||||
if (part.tool === "read") return read(props<typeof ReadTool>(part))
|
||||
if (part.tool === "write") return write(props<typeof WriteTool>(part))
|
||||
if (part.tool === "webfetch") return webfetch(props<typeof WebFetchTool>(part))
|
||||
if (part.tool === "edit") return edit(props<typeof EditTool>(part))
|
||||
if (part.tool === "codesearch") return codesearch(props<typeof CodeSearchTool>(part))
|
||||
if (part.tool === "websearch") return websearch(props<typeof WebSearchTool>(part))
|
||||
if (part.tool === "task") return task(props<typeof TaskTool>(part))
|
||||
if (part.tool === "todowrite") return todo(props<typeof TodoWriteTool>(part))
|
||||
if (part.tool === "skill") return skill(props<typeof SkillTool>(part))
|
||||
return fallback(part)
|
||||
}
|
||||
|
||||
const outputJsonEvent = (type: string, data: any) => {
|
||||
function emit(type: string, data: Record<string, unknown>) {
|
||||
if (args.format === "json") {
|
||||
process.stdout.write(JSON.stringify({ type, timestamp: Date.now(), sessionID, ...data }) + EOL)
|
||||
return true
|
||||
@@ -152,41 +401,77 @@ export const RunCommand = cmd({
|
||||
}
|
||||
|
||||
const events = await sdk.event.subscribe()
|
||||
let errorMsg: string | undefined
|
||||
let error: string | undefined
|
||||
|
||||
async function loop() {
|
||||
const toggles = new Map<string, boolean>()
|
||||
|
||||
const eventProcessor = (async () => {
|
||||
for await (const event of events.stream) {
|
||||
if (
|
||||
event.type === "message.updated" &&
|
||||
event.properties.info.role === "assistant" &&
|
||||
args.format !== "json" &&
|
||||
toggles.get("start") !== true
|
||||
) {
|
||||
UI.empty()
|
||||
UI.println(`> ${event.properties.info.agent} · ${event.properties.info.modelID}`)
|
||||
UI.empty()
|
||||
toggles.set("start", true)
|
||||
}
|
||||
|
||||
if (event.type === "message.part.updated") {
|
||||
const part = event.properties.part
|
||||
if (part.sessionID !== sessionID) continue
|
||||
|
||||
if (part.type === "tool" && part.state.status === "completed") {
|
||||
if (outputJsonEvent("tool_use", { part })) continue
|
||||
const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
|
||||
const title =
|
||||
part.state.title ||
|
||||
(Object.keys(part.state.input).length > 0 ? JSON.stringify(part.state.input) : "Unknown")
|
||||
printEvent(color, tool, title)
|
||||
if (part.tool === "bash" && part.state.output?.trim()) {
|
||||
UI.println()
|
||||
UI.println(part.state.output)
|
||||
}
|
||||
if (emit("tool_use", { part })) continue
|
||||
tool(part)
|
||||
}
|
||||
|
||||
if (
|
||||
part.type === "tool" &&
|
||||
part.tool === "task" &&
|
||||
part.state.status === "running" &&
|
||||
args.format !== "json"
|
||||
) {
|
||||
if (toggles.get(part.id) === true) continue
|
||||
task(props<typeof TaskTool>(part))
|
||||
toggles.set(part.id, true)
|
||||
}
|
||||
|
||||
if (part.type === "step-start") {
|
||||
if (outputJsonEvent("step_start", { part })) continue
|
||||
if (emit("step_start", { part })) continue
|
||||
}
|
||||
|
||||
if (part.type === "step-finish") {
|
||||
if (outputJsonEvent("step_finish", { part })) continue
|
||||
if (emit("step_finish", { part })) continue
|
||||
}
|
||||
|
||||
if (part.type === "text" && part.time?.end) {
|
||||
if (outputJsonEvent("text", { part })) continue
|
||||
const isPiped = !process.stdout.isTTY
|
||||
if (!isPiped) UI.println()
|
||||
process.stdout.write((isPiped ? part.text : UI.markdown(part.text)) + EOL)
|
||||
if (!isPiped) UI.println()
|
||||
if (emit("text", { part })) continue
|
||||
const text = part.text.trim()
|
||||
if (!text) continue
|
||||
if (!process.stdout.isTTY) {
|
||||
process.stdout.write(text + EOL)
|
||||
continue
|
||||
}
|
||||
UI.empty()
|
||||
UI.println(text)
|
||||
UI.empty()
|
||||
}
|
||||
|
||||
if (part.type === "reasoning" && part.time?.end && args.thinking) {
|
||||
if (emit("reasoning", { part })) continue
|
||||
const text = part.text.trim()
|
||||
if (!text) continue
|
||||
const line = `Thinking: ${text}`
|
||||
if (process.stdout.isTTY) {
|
||||
UI.empty()
|
||||
UI.println(`${UI.Style.TEXT_DIM}\u001b[3m${line}\u001b[0m${UI.Style.TEXT_NORMAL}`)
|
||||
UI.empty()
|
||||
continue
|
||||
}
|
||||
process.stdout.write(line + EOL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,42 +482,40 @@ export const RunCommand = cmd({
|
||||
if ("data" in props.error && props.error.data && "message" in props.error.data) {
|
||||
err = String(props.error.data.message)
|
||||
}
|
||||
errorMsg = errorMsg ? errorMsg + EOL + err : err
|
||||
if (outputJsonEvent("error", { error: props.error })) continue
|
||||
error = error ? error + EOL + err : err
|
||||
if (emit("error", { error: props.error })) continue
|
||||
UI.error(err)
|
||||
}
|
||||
|
||||
if (event.type === "session.idle" && event.properties.sessionID === sessionID) {
|
||||
if (
|
||||
event.type === "session.status" &&
|
||||
event.properties.sessionID === sessionID &&
|
||||
event.properties.status.type === "idle"
|
||||
) {
|
||||
break
|
||||
}
|
||||
|
||||
if (event.type === "permission.asked") {
|
||||
const permission = event.properties
|
||||
if (permission.sessionID !== sessionID) continue
|
||||
const result = await select({
|
||||
message: `Permission required: ${permission.permission} (${permission.patterns.join(", ")})`,
|
||||
options: [
|
||||
{ value: "once", label: "Allow once" },
|
||||
{ value: "always", label: "Always allow: " + permission.always.join(", ") },
|
||||
{ value: "reject", label: "Reject" },
|
||||
],
|
||||
initialValue: "once",
|
||||
}).catch(() => "reject")
|
||||
const response = (result.toString().includes("cancel") ? "reject" : result) as "once" | "always" | "reject"
|
||||
await sdk.permission.respond({
|
||||
sessionID,
|
||||
permissionID: permission.id,
|
||||
response,
|
||||
UI.println(
|
||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||
UI.Style.TEXT_NORMAL +
|
||||
`permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`,
|
||||
)
|
||||
await sdk.permission.reply({
|
||||
requestID: permission.id,
|
||||
reply: "reject",
|
||||
})
|
||||
}
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
// Validate agent if specified
|
||||
const resolvedAgent = await (async () => {
|
||||
const agent = await (async () => {
|
||||
if (!args.agent) return undefined
|
||||
const agent = await Agent.get(args.agent)
|
||||
if (!agent) {
|
||||
const entry = await Agent.get(args.agent)
|
||||
if (!entry) {
|
||||
UI.println(
|
||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||
UI.Style.TEXT_NORMAL,
|
||||
@@ -240,7 +523,7 @@ export const RunCommand = cmd({
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
if (agent.mode === "subagent") {
|
||||
if (entry.mode === "subagent") {
|
||||
UI.println(
|
||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||
UI.Style.TEXT_NORMAL,
|
||||
@@ -251,91 +534,42 @@ export const RunCommand = cmd({
|
||||
return args.agent
|
||||
})()
|
||||
|
||||
const sessionID = await session(sdk)
|
||||
if (!sessionID) {
|
||||
UI.error("Session not found")
|
||||
process.exit(1)
|
||||
}
|
||||
await share(sdk, sessionID)
|
||||
|
||||
loop().catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
if (args.command) {
|
||||
await sdk.session.command({
|
||||
sessionID,
|
||||
agent: resolvedAgent,
|
||||
agent,
|
||||
model: args.model,
|
||||
command: args.command,
|
||||
arguments: message,
|
||||
variant: args.variant,
|
||||
})
|
||||
} else {
|
||||
const modelParam = args.model ? Provider.parseModel(args.model) : undefined
|
||||
const model = args.model ? Provider.parseModel(args.model) : undefined
|
||||
await sdk.session.prompt({
|
||||
sessionID,
|
||||
agent: resolvedAgent,
|
||||
model: modelParam,
|
||||
agent,
|
||||
model,
|
||||
variant: args.variant,
|
||||
parts: [...fileParts, { type: "text", text: message }],
|
||||
parts: [...files, { type: "text", text: message }],
|
||||
})
|
||||
}
|
||||
|
||||
await eventProcessor
|
||||
if (errorMsg) process.exit(1)
|
||||
}
|
||||
|
||||
if (args.attach) {
|
||||
const sdk = createOpencodeClient({ baseUrl: args.attach })
|
||||
|
||||
const sessionID = await (async () => {
|
||||
if (args.continue) {
|
||||
const result = await sdk.session.list()
|
||||
return result.data?.find((s) => !s.parentID)?.id
|
||||
}
|
||||
if (args.session) return args.session
|
||||
|
||||
const title =
|
||||
args.title !== undefined
|
||||
? args.title === ""
|
||||
? message.slice(0, 50) + (message.length > 50 ? "..." : "")
|
||||
: args.title
|
||||
: undefined
|
||||
|
||||
const result = await sdk.session.create(
|
||||
title
|
||||
? {
|
||||
title,
|
||||
permission: [
|
||||
{
|
||||
permission: "question",
|
||||
action: "deny",
|
||||
pattern: "*",
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
permission: [
|
||||
{
|
||||
permission: "question",
|
||||
action: "deny",
|
||||
pattern: "*",
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
return result.data?.id
|
||||
})()
|
||||
|
||||
if (!sessionID) {
|
||||
UI.error("Session not found")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const cfgResult = await sdk.config.get()
|
||||
if (cfgResult.data && (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)) {
|
||||
const shareResult = await sdk.session.share({ sessionID }).catch((error) => {
|
||||
if (error instanceof Error && error.message.includes("disabled")) {
|
||||
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
|
||||
}
|
||||
return { error }
|
||||
})
|
||||
if (!shareResult.error && "data" in shareResult && shareResult.data?.share?.url) {
|
||||
UI.println(UI.Style.TEXT_INFO_BOLD + "~ " + shareResult.data.share.url)
|
||||
}
|
||||
}
|
||||
|
||||
return await execute(sdk, sessionID)
|
||||
return await execute(sdk)
|
||||
}
|
||||
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
@@ -344,52 +578,7 @@ export const RunCommand = cmd({
|
||||
return Server.App().fetch(request)
|
||||
}) as typeof globalThis.fetch
|
||||
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
|
||||
|
||||
if (args.command) {
|
||||
const exists = await Command.get(args.command)
|
||||
if (!exists) {
|
||||
UI.error(`Command "${args.command}" not found`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
const sessionID = await (async () => {
|
||||
if (args.continue) {
|
||||
const result = await sdk.session.list()
|
||||
return result.data?.find((s) => !s.parentID)?.id
|
||||
}
|
||||
if (args.session) return args.session
|
||||
|
||||
const title =
|
||||
args.title !== undefined
|
||||
? args.title === ""
|
||||
? message.slice(0, 50) + (message.length > 50 ? "..." : "")
|
||||
: args.title
|
||||
: undefined
|
||||
|
||||
const result = await sdk.session.create(title ? { title } : {})
|
||||
return result.data?.id
|
||||
})()
|
||||
|
||||
if (!sessionID) {
|
||||
UI.error("Session not found")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const cfgResult = await sdk.config.get()
|
||||
if (cfgResult.data && (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)) {
|
||||
const shareResult = await sdk.session.share({ sessionID }).catch((error) => {
|
||||
if (error instanceof Error && error.message.includes("disabled")) {
|
||||
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
|
||||
}
|
||||
return { error }
|
||||
})
|
||||
if (!shareResult.error && "data" in shareResult && shareResult.data?.share?.url) {
|
||||
UI.println(UI.Style.TEXT_INFO_BOLD + "~ " + shareResult.data.share.url)
|
||||
}
|
||||
}
|
||||
|
||||
await execute(sdk, sessionID)
|
||||
await execute(sdk)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -187,7 +187,6 @@ function App() {
|
||||
const route = useRoute()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const renderer = useRenderer()
|
||||
Clipboard.setRenderer(renderer)
|
||||
renderer.disableStdoutInterception()
|
||||
const dialog = useDialog()
|
||||
const local = useLocal()
|
||||
|
||||
@@ -129,6 +129,16 @@ export function Autocomplete(props: {
|
||||
return props.input().getTextRange(store.index + 1, props.input().cursorOffset)
|
||||
})
|
||||
|
||||
// filter() reads reactive props.value plus non-reactive cursor/text state.
|
||||
// On keypress those can be briefly out of sync, so filter() may return an empty/partial string.
|
||||
// Copy it into search in an effect because effects run after reactive updates have been rendered and painted
|
||||
// so the input has settled and all consumers read the same stable value.
|
||||
const [search, setSearch] = createSignal("")
|
||||
createEffect(() => {
|
||||
const next = filter()
|
||||
setSearch(next ? next : "")
|
||||
})
|
||||
|
||||
// When the filter changes due to how TUI works, the mousemove might still be triggered
|
||||
// via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard so
|
||||
// that the mouseover event doesn't trigger when filtering.
|
||||
@@ -208,7 +218,7 @@ export function Autocomplete(props: {
|
||||
}
|
||||
|
||||
const [files] = createResource(
|
||||
() => filter(),
|
||||
() => search(),
|
||||
async (query) => {
|
||||
if (!store.visible || store.visible === "/") return []
|
||||
|
||||
@@ -378,9 +388,9 @@ export function Autocomplete(props: {
|
||||
const mixed: AutocompleteOption[] =
|
||||
store.visible === "@" ? [...agentsValue, ...(filesValue || []), ...mcpResources()] : [...commandsValue]
|
||||
|
||||
const currentFilter = filter()
|
||||
const searchValue = search()
|
||||
|
||||
if (!currentFilter) {
|
||||
if (!searchValue) {
|
||||
return mixed
|
||||
}
|
||||
|
||||
@@ -388,7 +398,7 @@ export function Autocomplete(props: {
|
||||
return prev
|
||||
}
|
||||
|
||||
const result = fuzzysort.go(removeLineRange(currentFilter), mixed, {
|
||||
const result = fuzzysort.go(removeLineRange(searchValue), mixed, {
|
||||
keys: [
|
||||
(obj) => removeLineRange((obj.value ?? obj.display).trimEnd()),
|
||||
"description",
|
||||
@@ -398,7 +408,7 @@ export function Autocomplete(props: {
|
||||
scoreFn: (objResults) => {
|
||||
const displayResult = objResults[0]
|
||||
let score = objResults.score
|
||||
if (displayResult && displayResult.target.startsWith(store.visible + currentFilter)) {
|
||||
if (displayResult && displayResult.target.startsWith(store.visible + searchValue)) {
|
||||
score *= 2
|
||||
}
|
||||
const frecencyScore = objResults.obj.path ? frecency.getFrecency(objResults.obj.path) : 0
|
||||
|
||||
@@ -35,6 +35,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
const agent = iife(() => {
|
||||
const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
|
||||
const visibleAgents = createMemo(() => sync.data.agent.filter((x) => !x.hidden))
|
||||
const [agentStore, setAgentStore] = createStore<{
|
||||
current: string
|
||||
}>({
|
||||
@@ -48,6 +49,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
theme.warning,
|
||||
theme.primary,
|
||||
theme.error,
|
||||
theme.info,
|
||||
])
|
||||
return {
|
||||
list() {
|
||||
@@ -75,11 +77,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
},
|
||||
color(name: string) {
|
||||
const all = sync.data.agent
|
||||
const agent = all.find((x) => x.name === name)
|
||||
if (agent?.color) return RGBA.fromHex(agent.color)
|
||||
const index = all.findIndex((x) => x.name === name)
|
||||
const index = visibleAgents().findIndex((x) => x.name === name)
|
||||
if (index === -1) return colors()[0]
|
||||
const agent = visibleAgents()[index]
|
||||
|
||||
if (agent?.color) {
|
||||
const color = agent.color
|
||||
if (color.startsWith("#")) return RGBA.fromHex(color)
|
||||
// already validated by config, just satisfying TS here
|
||||
return theme[color as keyof typeof theme] as RGBA
|
||||
}
|
||||
return colors()[index % colors().length]
|
||||
},
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ import { useRenderer } from "@opentui/solid"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { useSDK } from "./sdk"
|
||||
|
||||
type ThemeColors = {
|
||||
primary: RGBA
|
||||
@@ -429,6 +428,7 @@ export function tint(base: RGBA, overlay: RGBA, alpha: number): RGBA {
|
||||
function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
|
||||
const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
|
||||
const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
|
||||
const transparent = RGBA.fromInts(0, 0, 0, 0)
|
||||
const isDark = mode == "dark"
|
||||
|
||||
const col = (i: number) => {
|
||||
@@ -479,8 +479,8 @@ function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJs
|
||||
textMuted,
|
||||
selectedListItemText: bg,
|
||||
|
||||
// Background colors
|
||||
background: bg,
|
||||
// Background colors - use transparent to respect terminal transparency
|
||||
background: transparent,
|
||||
backgroundPanel: grays[2],
|
||||
backgroundElement: grays[3],
|
||||
backgroundMenu: grays[3],
|
||||
|
||||
@@ -546,6 +546,7 @@ export function Session() {
|
||||
{
|
||||
title: showThinking() ? "Hide thinking" : "Show thinking",
|
||||
value: "session.toggle.thinking",
|
||||
keybind: "display_thinking",
|
||||
category: "Session",
|
||||
slash: {
|
||||
name: "thinking",
|
||||
@@ -1624,6 +1625,7 @@ function BlockTool(props: {
|
||||
function Bash(props: ToolProps<typeof BashTool>) {
|
||||
const { theme } = useTheme()
|
||||
const sync = useSync()
|
||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||
const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? ""))
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const lines = createMemo(() => output().split("\n"))
|
||||
@@ -1664,6 +1666,7 @@ function Bash(props: ToolProps<typeof BashTool>) {
|
||||
<BlockTool
|
||||
title={title()}
|
||||
part={props.part}
|
||||
spinner={isRunning()}
|
||||
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
||||
>
|
||||
<box gap={1}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createMemo, For, Show } from "solid-js"
|
||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||
import { useKeyboard } from "@opentui/solid"
|
||||
import type { TextareaRenderable } from "@opentui/core"
|
||||
import { useKeybind } from "../../context/keybind"
|
||||
@@ -19,6 +19,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
const questions = createMemo(() => props.request.questions)
|
||||
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
|
||||
const tabs = createMemo(() => (single() ? 1 : questions().length + 1)) // questions + confirm tab (no confirm for single select)
|
||||
const [tabHover, setTabHover] = createSignal<number | "confirm" | null>(null)
|
||||
const [store, setStore] = createStore({
|
||||
tab: 0,
|
||||
answers: [] as QuestionAnswer[],
|
||||
@@ -269,7 +270,15 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
backgroundColor={isActive() ? theme.accent : theme.backgroundElement}
|
||||
backgroundColor={
|
||||
isActive()
|
||||
? theme.accent
|
||||
: tabHover() === index()
|
||||
? theme.backgroundElement
|
||||
: theme.backgroundPanel
|
||||
}
|
||||
onMouseOver={() => setTabHover(index())}
|
||||
onMouseOut={() => setTabHover(null)}
|
||||
onMouseUp={() => selectTab(index())}
|
||||
>
|
||||
<text
|
||||
@@ -290,7 +299,11 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
backgroundColor={confirm() ? theme.accent : theme.backgroundElement}
|
||||
backgroundColor={
|
||||
confirm() ? theme.accent : tabHover() === "confirm" ? theme.backgroundElement : theme.backgroundPanel
|
||||
}
|
||||
onMouseOver={() => setTabHover("confirm")}
|
||||
onMouseOut={() => setTabHover(null)}
|
||||
onMouseUp={() => selectTab(questions().length)}
|
||||
>
|
||||
<text fg={confirm() ? selectedForeground(theme, theme.accent) : theme.textMuted}>Confirm</text>
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { $ } from "bun"
|
||||
import type { CliRenderer } from "@opentui/core"
|
||||
import { platform, release } from "os"
|
||||
import clipboardy from "clipboardy"
|
||||
import { lazy } from "../../../../util/lazy.js"
|
||||
import { tmpdir } from "os"
|
||||
import path from "path"
|
||||
|
||||
const rendererRef = { current: undefined as CliRenderer | undefined }
|
||||
/**
|
||||
* Writes text to clipboard via OSC 52 escape sequence.
|
||||
* This allows clipboard operations to work over SSH by having
|
||||
* the terminal emulator handle the clipboard locally.
|
||||
*/
|
||||
function writeOsc52(text: string): void {
|
||||
if (!process.stdout.isTTY) return
|
||||
const base64 = Buffer.from(text).toString("base64")
|
||||
const osc52 = `\x1b]52;c;${base64}\x07`
|
||||
const passthrough = process.env["TMUX"] || process.env["STY"]
|
||||
const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52
|
||||
process.stdout.write(sequence)
|
||||
}
|
||||
|
||||
export namespace Clipboard {
|
||||
export interface Content {
|
||||
@@ -14,10 +25,6 @@ export namespace Clipboard {
|
||||
mime: string
|
||||
}
|
||||
|
||||
export function setRenderer(renderer: CliRenderer | undefined): void {
|
||||
rendererRef.current = renderer
|
||||
}
|
||||
|
||||
export async function read(): Promise<Content | undefined> {
|
||||
const os = platform()
|
||||
|
||||
@@ -146,11 +153,7 @@ export namespace Clipboard {
|
||||
})
|
||||
|
||||
export async function copy(text: string): Promise<void> {
|
||||
const renderer = rendererRef.current
|
||||
if (renderer) {
|
||||
const copied = renderer.copyToClipboardOSC52(text)
|
||||
if (copied) return
|
||||
}
|
||||
writeOsc52(text)
|
||||
await getCopyMethod()(text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export const WebCommand = cmd({
|
||||
UI.println(
|
||||
UI.Style.TEXT_INFO_BOLD + " mDNS: ",
|
||||
UI.Style.TEXT_NORMAL,
|
||||
`opencode.local:${server.port}`,
|
||||
`${opts.mdnsDomain}:${server.port}`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ const options = {
|
||||
describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)",
|
||||
default: false,
|
||||
},
|
||||
"mdns-domain": {
|
||||
type: "string" as const,
|
||||
describe: "custom domain name for mDNS service (default: opencode.local)",
|
||||
default: "opencode.local",
|
||||
},
|
||||
cors: {
|
||||
type: "string" as const,
|
||||
array: true,
|
||||
@@ -36,9 +41,11 @@ export async function resolveNetworkOptions(args: NetworkOptions) {
|
||||
const portExplicitlySet = process.argv.includes("--port")
|
||||
const hostnameExplicitlySet = process.argv.includes("--hostname")
|
||||
const mdnsExplicitlySet = process.argv.includes("--mdns")
|
||||
const mdnsDomainExplicitlySet = process.argv.includes("--mdns-domain")
|
||||
const corsExplicitlySet = process.argv.includes("--cors")
|
||||
|
||||
const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
|
||||
const mdnsDomain = mdnsDomainExplicitlySet ? args["mdns-domain"] : (config?.server?.mdnsDomain ?? args["mdns-domain"])
|
||||
const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)
|
||||
const hostname = hostnameExplicitlySet
|
||||
? args.hostname
|
||||
@@ -49,5 +56,5 @@ export async function resolveNetworkOptions(args: NetworkOptions) {
|
||||
const argsCors = Array.isArray(args.cors) ? args.cors : args.cors ? [args.cors] : []
|
||||
const cors = [...configCors, ...argsCors]
|
||||
|
||||
return { hostname, port, mdns, cors }
|
||||
return { hostname, port, mdns, mdnsDomain, cors }
|
||||
}
|
||||
|
||||
@@ -24,10 +24,13 @@ import { LSPServer } from "../lsp/server"
|
||||
import { BunProc } from "@/bun"
|
||||
import { Installation } from "@/installation"
|
||||
import { ConfigMarkdown } from "./markdown"
|
||||
import { existsSync } from "fs"
|
||||
import { constants, existsSync } from "fs"
|
||||
import { Bus } from "@/bus"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Event } from "../server/event"
|
||||
import { PackageRegistry } from "@/bun/registry"
|
||||
import { proxied } from "@/util/proxied"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
@@ -142,9 +145,20 @@ export namespace Config {
|
||||
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
|
||||
}
|
||||
|
||||
const deps: Promise<void>[] = []
|
||||
|
||||
for (const dir of unique(directories)) {
|
||||
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
const files = ["opencode.jsonc", "opencode.json"]
|
||||
const isConfigDir = dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR || dir === Global.Path.config
|
||||
const configs = isConfigDir
|
||||
? await Promise.all(files.map((file) => loadFile(path.join(dir, file), { resolvePlugins: false })))
|
||||
: []
|
||||
const plugins = configs.flatMap((config) => config.plugin ?? [])
|
||||
const shouldInstall = await needsInstall(dir, plugins)
|
||||
if (shouldInstall) await installDependencies(dir, plugins)
|
||||
|
||||
if (isConfigDir) {
|
||||
for (const file of files) {
|
||||
log.debug(`loading config from ${path.join(dir, file)}`)
|
||||
result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file)))
|
||||
// to satisfy the type checker
|
||||
@@ -154,10 +168,6 @@ export namespace Config {
|
||||
}
|
||||
}
|
||||
|
||||
const exists = existsSync(path.join(dir, "node_modules"))
|
||||
const installing = installDependencies(dir)
|
||||
if (!exists) await installing
|
||||
|
||||
result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
|
||||
result.agent = mergeDeep(result.agent, await loadAgent(dir))
|
||||
result.agent = mergeDeep(result.agent, await loadMode(dir))
|
||||
@@ -230,30 +240,111 @@ export namespace Config {
|
||||
return {
|
||||
config: result,
|
||||
directories,
|
||||
deps,
|
||||
}
|
||||
})
|
||||
|
||||
export async function installDependencies(dir: string) {
|
||||
const pkg = path.join(dir, "package.json")
|
||||
export async function waitForDependencies() {
|
||||
const deps = await state().then((x) => x.deps)
|
||||
await Promise.all(deps)
|
||||
}
|
||||
|
||||
if (!(await Bun.file(pkg).exists())) {
|
||||
await Bun.write(pkg, "{}")
|
||||
export async function installDependencies(dir: string, plugins: string[] = []) {
|
||||
const pkg = path.join(dir, "package.json")
|
||||
const targetVersion = Installation.isLocal() ? "*" : Installation.VERSION
|
||||
|
||||
const json = await Bun.file(pkg)
|
||||
.json()
|
||||
.catch(() => ({}))
|
||||
json.dependencies = {
|
||||
...json.dependencies,
|
||||
"@opencode-ai/plugin": targetVersion,
|
||||
}
|
||||
const pluginDeps = deps(plugins)
|
||||
for (const [name, version] of Object.entries(pluginDeps)) {
|
||||
json.dependencies[name] = version
|
||||
}
|
||||
await Bun.write(pkg, JSON.stringify(json, null, 2))
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000))
|
||||
|
||||
const gitignore = path.join(dir, ".gitignore")
|
||||
const hasGitIgnore = await Bun.file(gitignore).exists()
|
||||
if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
|
||||
|
||||
await BunProc.run(
|
||||
["add", "@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION), "--exact"],
|
||||
{
|
||||
cwd: dir,
|
||||
},
|
||||
).catch(() => {})
|
||||
|
||||
// Install any additional dependencies defined in the package.json
|
||||
// This allows local plugins and custom tools to use external packages
|
||||
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
|
||||
await BunProc.run(
|
||||
[
|
||||
"install",
|
||||
// TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
|
||||
...(proxied() ? ["--no-cache"] : []),
|
||||
],
|
||||
{ cwd: dir },
|
||||
).catch(() => {})
|
||||
}
|
||||
|
||||
async function isWritable(dir: string) {
|
||||
try {
|
||||
await fs.access(dir, constants.W_OK)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function needsInstall(dir: string, plugins: string[] = []) {
|
||||
// Some config dirs may be read-only.
|
||||
// Installing deps there will fail; skip installation in that case.
|
||||
const writable = await isWritable(dir)
|
||||
if (!writable) {
|
||||
log.debug("config dir is not writable, skipping dependency install", { dir })
|
||||
return false
|
||||
}
|
||||
|
||||
const nodeModules = path.join(dir, "node_modules")
|
||||
if (!existsSync(nodeModules)) return true
|
||||
|
||||
const pkg = path.join(dir, "package.json")
|
||||
const pkgFile = Bun.file(pkg)
|
||||
const pkgExists = await pkgFile.exists()
|
||||
if (!pkgExists) return true
|
||||
|
||||
const parsed = await pkgFile.json().catch(() => null)
|
||||
const dependencies = parsed?.dependencies ?? {}
|
||||
const depVersion = dependencies["@opencode-ai/plugin"]
|
||||
if (!depVersion) return true
|
||||
|
||||
const targetVersion = Installation.isLocal() ? "latest" : Installation.VERSION
|
||||
if (targetVersion === "latest") {
|
||||
const isOutdated = await PackageRegistry.isOutdated("@opencode-ai/plugin", depVersion, dir)
|
||||
if (!isOutdated) return false
|
||||
log.info("Cached version is outdated, proceeding with install", {
|
||||
pkg: "@opencode-ai/plugin",
|
||||
cachedVersion: depVersion,
|
||||
})
|
||||
return true
|
||||
}
|
||||
if (depVersion !== targetVersion) return true
|
||||
|
||||
const pluginDeps = deps(plugins)
|
||||
for (const [name, version] of Object.entries(pluginDeps)) {
|
||||
if (dependencies[name] !== version) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function deps(items: string[]) {
|
||||
const result: Record<string, string> = {}
|
||||
for (const item of items) {
|
||||
if (!item) continue
|
||||
if (item.startsWith("file://")) continue
|
||||
if (item.startsWith("./") || item.startsWith("../") || item.startsWith("/") || item.startsWith("~")) continue
|
||||
const lastAt = item.lastIndexOf("@")
|
||||
const pkg = lastAt > 0 ? item.substring(0, lastAt) : item
|
||||
const version = lastAt > 0 ? item.substring(lastAt + 1) : "latest"
|
||||
result[pkg] = version
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function rel(item: string, patterns: string[]) {
|
||||
@@ -617,10 +708,12 @@ export namespace Config {
|
||||
.describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
|
||||
options: z.record(z.string(), z.any()).optional(),
|
||||
color: z
|
||||
.string()
|
||||
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")
|
||||
.union([
|
||||
z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"),
|
||||
z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||
])
|
||||
.optional()
|
||||
.describe("Hex color code for the agent (e.g., #FF5733)"),
|
||||
.describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"),
|
||||
steps: z
|
||||
.number()
|
||||
.int()
|
||||
@@ -835,6 +928,7 @@ export namespace Config {
|
||||
terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
|
||||
terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
|
||||
tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
|
||||
display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
@@ -860,6 +954,7 @@ export namespace Config {
|
||||
port: z.number().int().positive().optional().describe("Port to listen on"),
|
||||
hostname: z.string().optional().describe("Hostname to listen on"),
|
||||
mdns: z.boolean().optional().describe("Enable mDNS service discovery"),
|
||||
mdnsDomain: z.string().optional().describe("Custom domain name for mDNS service (default: opencode.local)"),
|
||||
cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"),
|
||||
})
|
||||
.strict()
|
||||
@@ -1146,7 +1241,7 @@ export namespace Config {
|
||||
return result
|
||||
})
|
||||
|
||||
async function loadFile(filepath: string): Promise<Info> {
|
||||
async function loadFile(filepath: string, options: { resolvePlugins?: boolean } = {}): Promise<Info> {
|
||||
log.info("loading", { path: filepath })
|
||||
let text = await Bun.file(filepath)
|
||||
.text()
|
||||
@@ -1155,10 +1250,10 @@ export namespace Config {
|
||||
throw new JsonError({ path: filepath }, { cause: err })
|
||||
})
|
||||
if (!text) return {}
|
||||
return load(text, filepath)
|
||||
return load(text, filepath, options)
|
||||
}
|
||||
|
||||
async function load(text: string, configFilepath: string) {
|
||||
async function load(text: string, configFilepath: string, options: { resolvePlugins?: boolean } = {}) {
|
||||
const original = text
|
||||
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
|
||||
return process.env[varName] || ""
|
||||
@@ -1234,7 +1329,7 @@ export namespace Config {
|
||||
await Bun.write(configFilepath, updated).catch(() => {})
|
||||
}
|
||||
const data = parsed.data
|
||||
if (data.plugin) {
|
||||
if (data.plugin && options.resolvePlugins !== false) {
|
||||
for (let i = 0; i < data.plugin.length; i++) {
|
||||
const plugin = data.plugin[i]
|
||||
try {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user