Compare commits
89
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2264c93b6b | ||
|
|
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
|
# web + desktop packages
|
||||||
packages/app/ @adamdotdevin
|
packages/app/ @adamdotdevin
|
||||||
packages/tauri/ @adamdotdevin
|
packages/tauri/ @adamdotdevin
|
||||||
|
packages/desktop/src-tauri/ @brendonovich
|
||||||
packages/desktop/ @adamdotdevin
|
packages/desktop/ @adamdotdevin
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
close-stale-prs:
|
close-stale-prs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Close inactive PRs
|
- name: Close inactive PRs
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v8
|
||||||
@@ -25,6 +26,15 @@ jobs:
|
|||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
const DAYS_INACTIVE = 60
|
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 cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000)
|
||||||
const { owner, repo } = context.repo
|
const { owner, repo } = context.repo
|
||||||
const dryRun = context.payload.inputs?.dryRun === "true"
|
const dryRun = context.payload.inputs?.dryRun === "true"
|
||||||
@@ -32,6 +42,42 @@ jobs:
|
|||||||
core.info(`Dry run mode: ${dryRun}`)
|
core.info(`Dry run mode: ${dryRun}`)
|
||||||
core.info(`Cutoff date: ${cutoff.toISOString()}`)
|
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 = `
|
const query = `
|
||||||
query($owner: String!, $repo: String!, $cursor: String) {
|
query($owner: String!, $repo: String!, $cursor: String) {
|
||||||
repository(owner: $owner, name: $repo) {
|
repository(owner: $owner, name: $repo) {
|
||||||
@@ -73,17 +119,27 @@ jobs:
|
|||||||
const allPrs = []
|
const allPrs = []
|
||||||
let cursor = null
|
let cursor = null
|
||||||
let hasNextPage = true
|
let hasNextPage = true
|
||||||
|
let pageCount = 0
|
||||||
|
|
||||||
while (hasNextPage) {
|
while (hasNextPage) {
|
||||||
const result = await github.graphql(query, {
|
pageCount++
|
||||||
owner,
|
core.info(`Fetching page ${pageCount} of open PRs...`)
|
||||||
repo,
|
|
||||||
cursor,
|
const result = await withRetry(
|
||||||
})
|
() => github.graphql(query, { owner, repo, cursor }),
|
||||||
|
`GraphQL page ${pageCount}`
|
||||||
|
)
|
||||||
|
|
||||||
allPrs.push(...result.repository.pullRequests.nodes)
|
allPrs.push(...result.repository.pullRequests.nodes)
|
||||||
hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage
|
hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage
|
||||||
cursor = result.repository.pullRequests.pageInfo.endCursor
|
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`)
|
core.info(`Found ${allPrs.length} open pull requests`)
|
||||||
@@ -114,28 +170,66 @@ jobs:
|
|||||||
|
|
||||||
core.info(`Found ${stalePrs.length} stale pull requests`)
|
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) {
|
for (const pr of stalePrs) {
|
||||||
const issue_number = pr.number
|
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.`
|
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) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
try {
|
||||||
owner,
|
// Add comment
|
||||||
repo,
|
await withRetry(
|
||||||
issue_number,
|
() => github.rest.issues.createComment({
|
||||||
body: closeComment,
|
owner,
|
||||||
})
|
repo,
|
||||||
|
issue_number,
|
||||||
|
body: closeComment,
|
||||||
|
}),
|
||||||
|
`Comment on PR #${issue_number}`
|
||||||
|
)
|
||||||
|
|
||||||
await github.rest.pulls.update({
|
// Close PR
|
||||||
owner,
|
await withRetry(
|
||||||
repo,
|
() => github.rest.pulls.update({
|
||||||
pull_number: issue_number,
|
owner,
|
||||||
state: "closed",
|
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
|
.env
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
.codex
|
||||||
*~
|
*~
|
||||||
playground
|
playground
|
||||||
tmp
|
tmp
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
// "plugin": ["opencode-openai-codex-auth"],
|
|
||||||
// "enterprise": {
|
// "enterprise": {
|
||||||
// "url": "https://enterprise.dev.opencode.ai",
|
// "url": "https://enterprise.dev.opencode.ai",
|
||||||
// },
|
// },
|
||||||
@@ -9,12 +8,7 @@
|
|||||||
"options": {},
|
"options": {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"mcp": {
|
"mcp": {},
|
||||||
"context7": {
|
|
||||||
"type": "remote",
|
|
||||||
"url": "https://mcp.context7.com/mcp",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"tools": {
|
"tools": {
|
||||||
"github-triage": false,
|
"github-triage": false,
|
||||||
"github-pr-search": 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
|
1. `$OPENCODE_INSTALL_DIR` - Custom installation directory
|
||||||
2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path
|
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
|
4. `$HOME/.opencode/bin` - Default fallback
|
||||||
|
|
||||||
```bash
|
```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.
|
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
|
- **plan** - Read-only agent for analysis and code exploration
|
||||||
- Denies file edits by default
|
- Denies file edits by default
|
||||||
- Asks permission before running bash commands
|
- Asks permission before running bash commands
|
||||||
- Ideal for exploring unfamiliar codebases or planning changes
|
- 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.
|
This is used internally and can be invoked using `@general` in messages.
|
||||||
|
|
||||||
Learn more about [agents](https://opencode.ai/docs/agents).
|
Learn more about [agents](https://opencode.ai/docs/agents).
|
||||||
|
|
||||||
### Documentation
|
### 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
|
### Contributing
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ If you're interested in contributing to OpenCode, please read our [contributing
|
|||||||
|
|
||||||
### Building on OpenCode
|
### 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
|
### 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:
|
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||||
|
|
||||||
- 100% open source
|
- 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.
|
- 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
|
- 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 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": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -258,7 +258,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -286,7 +286,7 @@
|
|||||||
"@ai-sdk/vercel": "1.0.33",
|
"@ai-sdk/vercel": "1.0.33",
|
||||||
"@ai-sdk/xai": "2.0.56",
|
"@ai-sdk/xai": "2.0.56",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@gitlab/gitlab-ai-provider": "3.3.1",
|
"@gitlab/gitlab-ai-provider": "3.4.0",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.25.2",
|
"@modelcontextprotocol/sdk": "1.25.2",
|
||||||
@@ -307,8 +307,9 @@
|
|||||||
"@standard-schema/spec": "1.0.0",
|
"@standard-schema/spec": "1.0.0",
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
|
"ai-gateway-provider": "2.3.1",
|
||||||
"bonjour-service": "1.3.0",
|
"bonjour-service": "1.3.0",
|
||||||
"bun-pty": "0.4.4",
|
"bun-pty": "0.4.8",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "4.0.3",
|
||||||
"clipboardy": "4.0.0",
|
"clipboardy": "4.0.0",
|
||||||
"decimal.js": "10.5.0",
|
"decimal.js": "10.5.0",
|
||||||
@@ -362,7 +363,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -382,7 +383,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.90.10",
|
"@hey-api/openapi-ts": "0.90.10",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -393,7 +394,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -406,7 +407,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -448,7 +449,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -459,7 +460,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -569,8 +570,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/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/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/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=="],
|
"@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=="],
|
||||||
@@ -925,7 +934,7 @@
|
|||||||
|
|
||||||
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
"@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=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
@@ -1949,6 +1958,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": ["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": ["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=="],
|
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||||
@@ -2097,7 +2108,7 @@
|
|||||||
|
|
||||||
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
"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=="],
|
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
||||||
|
|
||||||
@@ -3989,6 +4000,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/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/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=="],
|
"@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||||
@@ -4273,6 +4286,14 @@
|
|||||||
|
|
||||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"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=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
@@ -4805,6 +4826,14 @@
|
|||||||
|
|
||||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"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/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=="],
|
"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": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-4I0lpBnbAi7IZMURTMLysjrqdsNvXJf8802NrJnpdks=",
|
"x86_64-linux": "sha256-Uc9UFWrG9bVROt+DmXduXoY409wBBLtBe0G7R41NF8Q=",
|
||||||
"aarch64-linux": "sha256-WOGKsPlcQVSbL8TDr1JYO/2ucPTV2Hy0TXJKWv8EoVw=",
|
"aarch64-linux": "sha256-KTUsuPfWaw2qb26GmEa5tcSeF3+Kx2X5ZP5DE8jJuvQ=",
|
||||||
"aarch64-darwin": "sha256-LuvjwGm1QsHoLxuvSSp4VsDIv02Z/rTONsU32arQMuw=",
|
"aarch64-darwin": "sha256-C650/LVIoeymKnRw9lVO3f5ve9xYZPrO0vOM5pqY2nE=",
|
||||||
"x86_64-darwin": "sha256-AbglfgCWj/r+wHfle+e+D3b/xPcwwg4IK7j5iwn9nzw="
|
"x86_64-darwin": "sha256-xLLI2mNn222ktx6s8rwej3rMzQGl1S1jV/NXmLFg2DU="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test as base, expect } from "@playwright/test"
|
import { test as base, expect, type Page } from "@playwright/test"
|
||||||
import { seedProjects } from "./actions"
|
import { cleanupTestProject, createTestProject, seedProjects } from "./actions"
|
||||||
import { promptSelector } from "./selectors"
|
import { promptSelector } from "./selectors"
|
||||||
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
||||||
|
|
||||||
@@ -8,6 +8,14 @@ export const settingsKey = "settings.v3"
|
|||||||
type TestFixtures = {
|
type TestFixtures = {
|
||||||
sdk: ReturnType<typeof createSdk>
|
sdk: ReturnType<typeof createSdk>
|
||||||
gotoSession: (sessionID?: string) => Promise<void>
|
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 = {
|
type WorkerFixtures = {
|
||||||
@@ -33,17 +41,7 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
await use(createSdk(directory))
|
await use(createSdk(directory))
|
||||||
},
|
},
|
||||||
gotoSession: async ({ page, directory }, use) => {
|
gotoSession: async ({ page, directory }, use) => {
|
||||||
await seedProjects(page, { directory })
|
await seedStorage(page, { directory })
|
||||||
await page.addInitScript(() => {
|
|
||||||
localStorage.setItem(
|
|
||||||
"opencode.global.dat:model",
|
|
||||||
JSON.stringify({
|
|
||||||
recent: [{ providerID: "opencode", modelID: "big-pickle" }],
|
|
||||||
user: [],
|
|
||||||
variant: {},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const gotoSession = async (sessionID?: string) => {
|
const gotoSession = async (sessionID?: string) => {
|
||||||
await page.goto(sessionPath(directory, sessionID))
|
await page.goto(sessionPath(directory, sessionID))
|
||||||
@@ -51,6 +49,39 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
}
|
}
|
||||||
await use(gotoSession)
|
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 }
|
export { expect }
|
||||||
|
|||||||
@@ -1,52 +1,53 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openSidebar } from "../actions"
|
import { openSidebar } from "../actions"
|
||||||
|
|
||||||
test("dialog edit project updates name and startup script", async ({ page, gotoSession }) => {
|
test("dialog edit project updates name and startup script", async ({ page, withProject }) => {
|
||||||
await gotoSession()
|
|
||||||
await page.setViewportSize({ width: 1400, height: 800 })
|
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()
|
const header = page.locator(".group\\/project").first()
|
||||||
await header.hover()
|
await expect(header).toContainText(name)
|
||||||
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()
|
const reopened = await open()
|
||||||
await expect(menu).toBeVisible()
|
await expect(reopened.getByLabel("Name")).toHaveValue(name)
|
||||||
|
await expect(reopened.getByLabel("Workspace startup script")).toHaveValue(startup)
|
||||||
const editItem = menu.getByRole("menuitem", { name: "Edit" }).first()
|
await reopened.getByRole("button", { name: "Cancel" }).click()
|
||||||
await expect(editItem).toBeVisible()
|
await expect(reopened).toHaveCount(0)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,69 +1,73 @@
|
|||||||
import { test, expect } from "../fixtures"
|
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 { projectCloseHoverSelector, projectCloseMenuSelector, projectSwitchSelector } from "../selectors"
|
||||||
import { dirSlug } from "../utils"
|
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 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
const other = await createTestProject()
|
const other = await createTestProject()
|
||||||
const otherSlug = dirSlug(other)
|
const otherSlug = dirSlug(other)
|
||||||
await seedProjects(page, { directory, extra: [other] })
|
|
||||||
|
|
||||||
try {
|
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()
|
const close = page.locator(projectCloseHoverSelector(otherSlug)).first()
|
||||||
await expect(otherButton).toBeVisible()
|
await expect(close).toBeVisible()
|
||||||
await otherButton.hover()
|
await close.click()
|
||||||
|
|
||||||
const close = page.locator(projectCloseHoverSelector(otherSlug)).first()
|
await expect(otherButton).toHaveCount(0)
|
||||||
await expect(close).toBeVisible()
|
},
|
||||||
await close.click()
|
{ extra: [other] },
|
||||||
|
)
|
||||||
await expect(otherButton).toHaveCount(0)
|
|
||||||
} finally {
|
} finally {
|
||||||
await cleanupTestProject(other)
|
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 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
const other = await createTestProject()
|
const other = await createTestProject()
|
||||||
const otherName = other.split("/").pop() ?? other
|
const otherName = other.split("/").pop() ?? other
|
||||||
const otherSlug = dirSlug(other)
|
const otherSlug = dirSlug(other)
|
||||||
await seedProjects(page, { directory, extra: [other] })
|
|
||||||
|
|
||||||
try {
|
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(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
||||||
await expect(otherButton).toBeVisible()
|
|
||||||
await otherButton.click()
|
|
||||||
|
|
||||||
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
|
const trigger = header.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`).first()
|
||||||
.locator(".group\\/project")
|
await expect(trigger).toHaveCount(1)
|
||||||
.filter({ has: page.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`) })
|
await trigger.focus()
|
||||||
.first()
|
await page.keyboard.press("Enter")
|
||||||
await expect(header).toContainText(otherName)
|
|
||||||
|
|
||||||
const trigger = header.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`).first()
|
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
|
||||||
await expect(trigger).toHaveCount(1)
|
await expect(menu).toBeVisible({ timeout: 10_000 })
|
||||||
await trigger.focus()
|
|
||||||
await page.keyboard.press("Enter")
|
|
||||||
|
|
||||||
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
|
await clickMenuItem(menu, /^Close$/i, { force: true })
|
||||||
await expect(menu).toBeVisible({ timeout: 10_000 })
|
await expect(otherButton).toHaveCount(0)
|
||||||
|
},
|
||||||
await clickMenuItem(menu, /^Close$/i, { force: true })
|
{ extra: [other] },
|
||||||
await expect(otherButton).toHaveCount(0)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
await cleanupTestProject(other)
|
await cleanupTestProject(other)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { defocus, createTestProject, seedProjects, cleanupTestProject } from "../actions"
|
import { defocus, createTestProject, cleanupTestProject } from "../actions"
|
||||||
import { projectSwitchSelector } from "../selectors"
|
import { projectSwitchSelector } from "../selectors"
|
||||||
import { dirSlug } from "../utils"
|
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 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
const other = await createTestProject()
|
const other = await createTestProject()
|
||||||
const otherSlug = dirSlug(other)
|
const otherSlug = dirSlug(other)
|
||||||
|
|
||||||
await seedProjects(page, { directory, extra: [other] })
|
|
||||||
|
|
||||||
try {
|
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)
|
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
||||||
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
|
||||||
await expect(otherButton).toBeVisible()
|
|
||||||
await otherButton.click()
|
|
||||||
|
|
||||||
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(page).toHaveURL(new RegExp(`/${currentSlug}/session`))
|
||||||
await expect(currentButton).toBeVisible()
|
},
|
||||||
await currentButton.click()
|
{ extra: [other] },
|
||||||
|
)
|
||||||
await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`))
|
|
||||||
} finally {
|
} finally {
|
||||||
await cleanupTestProject(other)
|
await cleanupTestProject(other)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,33 +10,20 @@ import {
|
|||||||
cleanupTestProject,
|
cleanupTestProject,
|
||||||
clickMenuItem,
|
clickMenuItem,
|
||||||
confirmDialog,
|
confirmDialog,
|
||||||
createTestProject,
|
|
||||||
openSidebar,
|
openSidebar,
|
||||||
openWorkspaceMenu,
|
openWorkspaceMenu,
|
||||||
seedProjects,
|
|
||||||
setWorkspacesEnabled,
|
setWorkspacesEnabled,
|
||||||
} from "../actions"
|
} from "../actions"
|
||||||
import { inlineInputSelector, projectSwitchSelector, workspaceItemSelector } from "../selectors"
|
import { inlineInputSelector, workspaceItemSelector } from "../selectors"
|
||||||
import { dirSlug } from "../utils"
|
|
||||||
|
|
||||||
function slugFromUrl(url: string) {
|
function slugFromUrl(url: string) {
|
||||||
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupWorkspaceTest(page: Page, directory: string, gotoSession: () => Promise<void>) {
|
async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
||||||
const project = await createTestProject()
|
const rootSlug = project.slug
|
||||||
const rootSlug = dirSlug(project)
|
|
||||||
await seedProjects(page, { directory, extra: [project] })
|
|
||||||
|
|
||||||
await gotoSession()
|
|
||||||
await openSidebar(page)
|
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 setWorkspacesEnabled(page, rootSlug, true)
|
||||||
|
|
||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
@@ -70,25 +57,13 @@ async function setupWorkspaceTest(page: Page, directory: string, gotoSession: ()
|
|||||||
)
|
)
|
||||||
.toBe(true)
|
.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 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
const project = await createTestProject()
|
await withProject(async ({ slug }) => {
|
||||||
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 openSidebar(page)
|
await openSidebar(page)
|
||||||
|
|
||||||
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
|
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 setWorkspacesEnabled(page, slug, false)
|
||||||
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
|
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
|
||||||
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
|
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 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
const project = await createTestProject()
|
await withProject(async ({ slug }) => {
|
||||||
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 openSidebar(page)
|
await openSidebar(page)
|
||||||
await setWorkspacesEnabled(page, slug, true)
|
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 expect(page.locator(workspaceItemSelector(workspaceSlug)).first()).toBeVisible()
|
||||||
|
|
||||||
await cleanupTestProject(workspaceDir)
|
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 })
|
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 rename = `e2e workspace ${Date.now()}`
|
||||||
const menu = await openWorkspaceMenu(page, slug)
|
const menu = await openWorkspaceMenu(page, slug)
|
||||||
await clickMenuItem(menu, /^Rename$/i, { force: true })
|
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.fill(rename)
|
||||||
await input.press("Enter")
|
await input.press("Enter")
|
||||||
await expect(item).toContainText(rename)
|
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 })
|
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 readme = path.join(createdDir, "README.md")
|
||||||
const extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`)
|
const extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`)
|
||||||
const original = await fs.readFile(readme, "utf8")
|
const original = await fs.readFile(readme, "utf8")
|
||||||
@@ -250,17 +207,15 @@ test("can reset a workspace", async ({ page, directory, sdk, gotoSession }) => {
|
|||||||
.catch(() => false)
|
.catch(() => false)
|
||||||
})
|
})
|
||||||
.toBe(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 })
|
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)
|
const menu = await openWorkspaceMenu(page, slug)
|
||||||
await clickMenuItem(menu, /^Delete$/i, { force: true })
|
await clickMenuItem(menu, /^Delete$/i, { force: true })
|
||||||
await confirmDialog(page, /^Delete workspace$/i)
|
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).toHaveURL(new RegExp(`/${rootSlug}/session`))
|
||||||
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
|
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
|
||||||
await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
|
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 page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
await withProject(async ({ slug: rootSlug }) => {
|
||||||
|
const workspaces = [] as { directory: string; slug: string }[]
|
||||||
|
|
||||||
const project = await createTestProject()
|
const listSlugs = async () => {
|
||||||
const rootSlug = dirSlug(project)
|
const nodes = page.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]')
|
||||||
await seedProjects(page, { directory, extra: [project] })
|
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 waitReady = async (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()
|
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
() => {
|
async () => {
|
||||||
const slug = slugFromUrl(page.url())
|
const item = page.locator(workspaceItemSelector(slug)).first()
|
||||||
return slug.length > 0 && slug !== rootSlug && slug !== prev
|
try {
|
||||||
|
await item.hover({ timeout: 500 })
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ timeout: 45_000 },
|
{ timeout: 60_000 },
|
||||||
)
|
)
|
||||||
.toBe(true)
|
.toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
const slug = slugFromUrl(page.url())
|
const drag = async (from: string, to: string) => {
|
||||||
const dir = base64Decode(slug)
|
const src = page.locator(workspaceItemSelector(from)).first()
|
||||||
workspaces.push({ slug, directory: dir })
|
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 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)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost"
|
|||||||
const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||||
const command = `bun run dev -- --host 0.0.0.0 --port ${port}`
|
const command = `bun run dev -- --host 0.0.0.0 --port ${port}`
|
||||||
const reuse = !process.env.CI
|
const reuse = !process.env.CI
|
||||||
const win = process.platform === "win32"
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: "./e2e",
|
testDir: "./e2e",
|
||||||
@@ -15,8 +14,7 @@ export default defineConfig({
|
|||||||
expect: {
|
expect: {
|
||||||
timeout: 10_000,
|
timeout: 10_000,
|
||||||
},
|
},
|
||||||
fullyParallel: !win,
|
fullyParallel: true,
|
||||||
workers: win ? 1 : undefined,
|
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]],
|
reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]],
|
||||||
|
|||||||
@@ -241,19 +241,19 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
protected _rowEnd(row: number, isLastRow: boolean): void {
|
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||||
let rowSeparator = ""
|
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._currentRow += " ".repeat(this._nullCellCount)
|
||||||
this._nullCellCount = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLastRow) {
|
this._nullCellCount = 0
|
||||||
const nextLine = this._buffer.getLine(row + 1)
|
|
||||||
|
|
||||||
if (!nextLine?.isWrapped) {
|
if (!isLastRow && !wrapped) {
|
||||||
rowSeparator = "\r\n"
|
rowSeparator = "\r\n"
|
||||||
this._lastCursorRow = row + 1
|
this._lastCursorRow = row + 1
|
||||||
this._lastCursorCol = 0
|
this._lastCursorCol = 0
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._allRows[this._rowIndex] = this._currentRow
|
this._allRows[this._rowIndex] = this._currentRow
|
||||||
@@ -389,7 +389,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
|
|
||||||
const sgrSeq = this._diffStyle(cell, this._cursorStyle)
|
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 (styleChanged) {
|
||||||
if (this._nullCellCount > 0) {
|
if (this._nullCellCount > 0) {
|
||||||
@@ -442,12 +442,24 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!excludeFinalCursorPosition) {
|
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 absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY
|
||||||
const cursorCol = this._buffer.cursorX + 1
|
const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER)
|
||||||
content += `\u001b[${cursorRow};${cursorCol}H`
|
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
|
return content
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (store.authorization?.method === "code" && store.authorization?.url) {
|
if (store.authorization?.method === "code" && store.authorization?.url) {
|
||||||
platform.openLink(store.authorization.url)
|
void platform.openLink(store.authorization.url).catch(() => undefined)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -396,7 +396,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (store.authorization?.url) {
|
if (store.authorization?.url) {
|
||||||
platform.openLink(store.authorization.url)
|
void platform.openLink(store.authorization.url).catch(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await globalSDK.client.provider.oauth
|
const result = await globalSDK.client.provider.oauth
|
||||||
|
|||||||
@@ -158,22 +158,22 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<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={{
|
classList={{
|
||||||
"opacity-100": store.iconHover && !store.iconUrl,
|
"opacity-100": store.iconHover && !store.iconUrl,
|
||||||
"opacity-0": !(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>
|
||||||
<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={{
|
classList={{
|
||||||
"opacity-100": store.iconHover && !!store.iconUrl,
|
"opacity-100": store.iconHover && !!store.iconUrl,
|
||||||
"opacity-0": !(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>
|
||||||
</div>
|
</div>
|
||||||
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
|
<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 { List } from "@opencode-ai/ui/list"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo, createResource, createSignal } from "solid-js"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import type { ListRef } from "@opencode-ai/ui/list"
|
||||||
|
|
||||||
interface DialogSelectDirectoryProps {
|
interface DialogSelectDirectoryProps {
|
||||||
title?: string
|
title?: string
|
||||||
@@ -15,18 +16,47 @@ interface DialogSelectDirectoryProps {
|
|||||||
onSelect: (result: string | string[] | null) => void
|
onSelect: (result: string | string[] | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Row = {
|
||||||
|
absolute: string
|
||||||
|
search: string
|
||||||
|
}
|
||||||
|
|
||||||
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||||
const sync = useGlobalSync()
|
const sync = useGlobalSync()
|
||||||
const sdk = useGlobalSDK()
|
const sdk = useGlobalSDK()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
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 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) {
|
function normalize(input: string) {
|
||||||
const v = input.replaceAll("\\", "/")
|
const v = input.replaceAll("\\", "/")
|
||||||
if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/")
|
if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/")
|
||||||
@@ -64,24 +94,67 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||||||
return ""
|
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)
|
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()
|
const h = home()
|
||||||
if (!h) return full
|
if (!h) return ""
|
||||||
|
|
||||||
const hn = trimTrailing(h)
|
const hn = trimTrailing(h)
|
||||||
const lc = full.toLowerCase()
|
const lc = full.toLowerCase()
|
||||||
const hc = hn.toLowerCase()
|
const hc = hn.toLowerCase()
|
||||||
if (lc === hc) return "~"
|
if (lc === hc) return "~"
|
||||||
if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length)
|
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()
|
const base = start()
|
||||||
if (!base) return
|
if (!base) return
|
||||||
|
|
||||||
const raw = normalizeDriveRoot(filter.trim())
|
const raw = normalizeDriveRoot(value)
|
||||||
if (!raw) return { directory: trimTrailing(base), path: "" }
|
if (!raw) return { directory: trimTrailing(base), path: "" }
|
||||||
|
|
||||||
const h = home()
|
const h = home()
|
||||||
@@ -122,21 +195,25 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const directories = async (filter: string) => {
|
const directories = async (filter: string) => {
|
||||||
const input = scoped(filter)
|
const value = clean(filter)
|
||||||
if (!input) return [] as string[]
|
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 isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/")
|
||||||
|
|
||||||
const query = normalizeDriveRoot(input.path)
|
const query = normalizeDriveRoot(scopedInput.path)
|
||||||
|
|
||||||
if (!isPath) {
|
const find = () =>
|
||||||
const results = await sdk.client.find
|
sdk.client.find
|
||||||
.files({ directory: input.directory, query, type: "directory", limit: 50 })
|
.files({ directory: scopedInput.directory, query, type: "directory", limit: 50 })
|
||||||
.then((x) => x.data ?? [])
|
.then((x) => x.data ?? [])
|
||||||
.catch(() => [])
|
.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("/")
|
const segments = query.replace(/^\/+/, "").split("/")
|
||||||
@@ -145,17 +222,10 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||||||
|
|
||||||
const cap = 12
|
const cap = 12
|
||||||
const branch = 4
|
const branch = 4
|
||||||
let paths = [input.directory]
|
let paths = [scopedInput.directory]
|
||||||
for (const part of head) {
|
for (const part of head) {
|
||||||
if (part === "..") {
|
if (part === "..") {
|
||||||
paths = paths.map((p) => {
|
paths = paths.map(parentOf)
|
||||||
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)
|
|
||||||
})
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +235,27 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const out = (await Promise.all(paths.map((p) => match(p, tail, 50)))).flat()
|
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) {
|
function resolve(absolute: string) {
|
||||||
@@ -179,24 +269,52 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||||||
search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
|
search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
|
||||||
emptyMessage={language.t("dialog.directory.empty")}
|
emptyMessage={language.t("dialog.directory.empty")}
|
||||||
loadingMessage={language.t("common.loading")}
|
loadingMessage={language.t("common.loading")}
|
||||||
items={directories}
|
items={items}
|
||||||
key={(x) => x}
|
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) => {
|
onSelect={(path) => {
|
||||||
if (!path) return
|
if (!path) return
|
||||||
resolve(path)
|
resolve(path.absolute)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(absolute) => {
|
{(item) => {
|
||||||
const path = display(absolute)
|
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 (
|
return (
|
||||||
<div class="w-full flex items-center justify-between rounded-md">
|
<div class="w-full flex items-center justify-between rounded-md">
|
||||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
<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">
|
<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">
|
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||||
{getDirectory(path)}
|
{getDirectory(path)}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-text-strong whitespace-nowrap">{getFilename(path)}</span>
|
<span class="text-text-strong whitespace-nowrap">{getFilename(path)}</span>
|
||||||
|
<span class="text-text-weak whitespace-nowrap">/</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { createMemo, createSignal, onCleanup, Show } from "solid-js"
|
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
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 { useLayout } from "@/context/layout"
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { decode64 } from "@/utils/base64"
|
||||||
|
|
||||||
type EntryType = "command" | "file"
|
type EntryType = "command" | "file" | "session"
|
||||||
|
|
||||||
type Entry = {
|
type Entry = {
|
||||||
id: string
|
id: string
|
||||||
@@ -22,6 +27,9 @@ type Entry = {
|
|||||||
category: string
|
category: string
|
||||||
option?: CommandOption
|
option?: CommandOption
|
||||||
path?: string
|
path?: string
|
||||||
|
directory?: string
|
||||||
|
sessionID?: string
|
||||||
|
archived?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type DialogSelectFileMode = "all" | "files"
|
type DialogSelectFileMode = "all" | "files"
|
||||||
@@ -33,6 +41,9 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
const file = useFile()
|
const file = useFile()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const globalSDK = useGlobalSDK()
|
||||||
|
const globalSync = useGlobalSync()
|
||||||
const filesOnly = () => props.mode === "files"
|
const filesOnly = () => props.mode === "files"
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
@@ -73,6 +84,52 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
path,
|
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 list = createMemo(() => allowed().map(commandItem))
|
||||||
|
|
||||||
const picks = createMemo(() => {
|
const picks = createMemo(() => {
|
||||||
@@ -122,6 +179,68 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
return out
|
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 items = async (text: string) => {
|
||||||
const query = text.trim()
|
const query = text.trim()
|
||||||
setGrouped(query.length > 0)
|
setGrouped(query.length > 0)
|
||||||
@@ -146,9 +265,10 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
const files = await file.searchFiles(query)
|
const files = await file.searchFiles(query)
|
||||||
return files.map(fileItem)
|
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)
|
const entries = files.map(fileItem)
|
||||||
return [...list(), ...entries]
|
return [...list(), ...nextSessions, ...entries]
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMove = (item: Entry | undefined) => {
|
const handleMove = (item: Entry | undefined) => {
|
||||||
@@ -178,6 +298,12 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.type === "session") {
|
||||||
|
if (!item.directory || !item.sessionID) return
|
||||||
|
navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!item.path) return
|
if (!item.path) return
|
||||||
open(item.path)
|
open(item.path)
|
||||||
}
|
}
|
||||||
@@ -202,13 +328,12 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
items={items}
|
items={items}
|
||||||
key={(item) => item.id}
|
key={(item) => item.id}
|
||||||
filterKeys={["title", "description", "category"]}
|
filterKeys={["title", "description", "category"]}
|
||||||
groupBy={(item) => item.category}
|
groupBy={grouped() ? (item) => item.category : () => ""}
|
||||||
onMove={handleMove}
|
onMove={handleMove}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
>
|
>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<Show
|
<Switch
|
||||||
when={item.type === "command"}
|
|
||||||
fallback={
|
fallback={
|
||||||
<div class="w-full flex items-center justify-between rounded-md pl-1">
|
<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">
|
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||||
@@ -223,18 +348,43 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="w-full flex items-center justify-between gap-4">
|
<Match when={item.type === "command"}>
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="w-full flex items-center justify-between gap-4">
|
||||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<Show when={item.description}>
|
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
||||||
<span class="text-14-regular text-text-weak truncate">{item.description}</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>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={item.keybind}>
|
</Match>
|
||||||
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
|
<Match when={item.type === "session"}>
|
||||||
</Show>
|
<div class="w-full flex items-center justify-between rounded-md pl-1">
|
||||||
</div>
|
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||||
</Show>
|
<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>
|
</List>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ const ModelList: Component<{
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
placement="right-start"
|
placement="right-start"
|
||||||
gutter={12}
|
gutter={12}
|
||||||
forceMount={false}
|
|
||||||
value={
|
value={
|
||||||
<ModelTooltip
|
<ModelTooltip
|
||||||
model={item}
|
model={item}
|
||||||
@@ -90,10 +89,9 @@ const ModelList: Component<{
|
|||||||
|
|
||||||
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
||||||
provider?: string
|
provider?: string
|
||||||
children?: JSX.Element | ((open: boolean) => JSX.Element)
|
children?: JSX.Element
|
||||||
triggerAs?: T
|
triggerAs?: T
|
||||||
triggerProps?: ComponentProps<T>
|
triggerProps?: ComponentProps<T>
|
||||||
gutter?: number
|
|
||||||
}) {
|
}) {
|
||||||
const [store, setStore] = createStore<{
|
const [store, setStore] = createStore<{
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -176,14 +174,14 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
|||||||
}}
|
}}
|
||||||
modal={false}
|
modal={false}
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
gutter={props.gutter ?? 8}
|
gutter={8}
|
||||||
>
|
>
|
||||||
<Kobalte.Trigger
|
<Kobalte.Trigger
|
||||||
ref={(el) => setStore("trigger", el)}
|
ref={(el) => setStore("trigger", el)}
|
||||||
as={props.triggerAs ?? "div"}
|
as={props.triggerAs ?? "div"}
|
||||||
{...(props.triggerProps as any)}
|
{...(props.triggerProps as any)}
|
||||||
>
|
>
|
||||||
{typeof props.children === "function" ? props.children(store.open) : props.children}
|
{props.children}
|
||||||
</Kobalte.Trigger>
|
</Kobalte.Trigger>
|
||||||
<Kobalte.Portal>
|
<Kobalte.Portal>
|
||||||
<Kobalte.Content
|
<Kobalte.Content
|
||||||
@@ -215,7 +213,7 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
|||||||
class="p-1"
|
class="p-1"
|
||||||
action={
|
action={
|
||||||
<div class="flex items-center gap-1">
|
<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
|
<IconButton
|
||||||
icon="plus-small"
|
icon="plus-small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -225,7 +223,7 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
|||||||
onClick={handleConnectProvider}
|
onClick={handleConnectProvider}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement="top" forceMount={false} value={language.t("dialog.model.manage")}>
|
<Tooltip placement="top" value={language.t("dialog.model.manage")}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="sliders"
|
icon="sliders"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -130,10 +130,57 @@ export default function FileTree(props: {
|
|||||||
const nodes = file.tree.children(props.path)
|
const nodes = file.tree.children(props.path)
|
||||||
const current = filter()
|
const current = filter()
|
||||||
if (!current) return nodes
|
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)
|
if (node.type === "file") return current.files.has(node.path)
|
||||||
return current.dirs.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 = (
|
const Node = (
|
||||||
@@ -274,7 +321,6 @@ export default function FileTree(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
forceMount={false}
|
|
||||||
openDelay={2000}
|
openDelay={2000}
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ export function Link(props: LinkProps) {
|
|||||||
const [local, rest] = splitProps(props, ["href", "children"])
|
const [local, rest] = splitProps(props, ["href", "children"])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button class="text-text-strong underline" onClick={() => platform.openLink(local.href)} {...rest}>
|
<button
|
||||||
|
class="text-text-strong underline"
|
||||||
|
onClick={() => void platform.openLink(local.href).catch(() => undefined)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
{local.children}
|
{local.children}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ import { useNavigate, useParams } from "@solidjs/router"
|
|||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { MorphChevron } from "@opencode-ai/ui/morph-chevron"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { CycleLabel } from "@opencode-ai/ui/cycle-label"
|
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
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 { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/util/path"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||||
import { ReasoningIcon } from "@opencode-ai/ui/reasoning-icon"
|
|
||||||
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
||||||
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
@@ -1135,7 +1132,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const images = imageAttachments().slice()
|
const images = imageAttachments().slice()
|
||||||
const mode = store.mode
|
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()
|
if (working()) abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1257,7 +1254,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
clearInput()
|
clearInput()
|
||||||
client.session
|
client.session
|
||||||
.shell({
|
.shell({
|
||||||
sessionID: session?.id || "",
|
sessionID: session.id,
|
||||||
agent,
|
agent,
|
||||||
model,
|
model,
|
||||||
command: text,
|
command: text,
|
||||||
@@ -1280,7 +1277,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
clearInput()
|
clearInput()
|
||||||
client.session
|
client.session
|
||||||
.command({
|
.command({
|
||||||
sessionID: session?.id || "",
|
sessionID: session.id,
|
||||||
command: commandName,
|
command: commandName,
|
||||||
arguments: args.join(" "),
|
arguments: args.join(" "),
|
||||||
agent,
|
agent,
|
||||||
@@ -1436,13 +1433,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
const optimisticParts = requestParts.map((part) => ({
|
const optimisticParts = requestParts.map((part) => ({
|
||||||
...part,
|
...part,
|
||||||
sessionID: session?.id || "",
|
sessionID: session.id,
|
||||||
messageID,
|
messageID,
|
||||||
})) as unknown as Part[]
|
})) as unknown as Part[]
|
||||||
|
|
||||||
const optimisticMessage: Message = {
|
const optimisticMessage: Message = {
|
||||||
id: messageID,
|
id: messageID,
|
||||||
sessionID: session?.id || "",
|
sessionID: session.id,
|
||||||
role: "user",
|
role: "user",
|
||||||
time: { created: Date.now() },
|
time: { created: Date.now() },
|
||||||
agent,
|
agent,
|
||||||
@@ -1453,9 +1450,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
if (sessionDirectory === projectDirectory) {
|
if (sessionDirectory === projectDirectory) {
|
||||||
sync.set(
|
sync.set(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const messages = draft.message[session?.id || ""]
|
const messages = draft.message[session.id]
|
||||||
if (!messages) {
|
if (!messages) {
|
||||||
draft.message[session?.id || ""] = [optimisticMessage]
|
draft.message[session.id] = [optimisticMessage]
|
||||||
} else {
|
} else {
|
||||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||||
messages.splice(result.index, 0, optimisticMessage)
|
messages.splice(result.index, 0, optimisticMessage)
|
||||||
@@ -1463,7 +1460,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
draft.part[messageID] = optimisticParts
|
draft.part[messageID] = optimisticParts
|
||||||
.filter((p) => !!p?.id)
|
.filter((p) => !!p?.id)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -1471,9 +1468,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
globalSync.child(sessionDirectory)[1](
|
globalSync.child(sessionDirectory)[1](
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const messages = draft.message[session?.id || ""]
|
const messages = draft.message[session.id]
|
||||||
if (!messages) {
|
if (!messages) {
|
||||||
draft.message[session?.id || ""] = [optimisticMessage]
|
draft.message[session.id] = [optimisticMessage]
|
||||||
} else {
|
} else {
|
||||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||||
messages.splice(result.index, 0, optimisticMessage)
|
messages.splice(result.index, 0, optimisticMessage)
|
||||||
@@ -1481,7 +1478,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
draft.part[messageID] = optimisticParts
|
draft.part[messageID] = optimisticParts
|
||||||
.filter((p) => !!p?.id)
|
.filter((p) => !!p?.id)
|
||||||
.slice()
|
.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 +1487,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
if (sessionDirectory === projectDirectory) {
|
if (sessionDirectory === projectDirectory) {
|
||||||
sync.set(
|
sync.set(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const messages = draft.message[session?.id || ""]
|
const messages = draft.message[session.id]
|
||||||
if (messages) {
|
if (messages) {
|
||||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||||
if (result.found) messages.splice(result.index, 1)
|
if (result.found) messages.splice(result.index, 1)
|
||||||
@@ -1503,7 +1500,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
globalSync.child(sessionDirectory)[1](
|
globalSync.child(sessionDirectory)[1](
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const messages = draft.message[session?.id || ""]
|
const messages = draft.message[session.id]
|
||||||
if (messages) {
|
if (messages) {
|
||||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||||
if (result.found) messages.splice(result.index, 1)
|
if (result.found) messages.splice(result.index, 1)
|
||||||
@@ -1524,15 +1521,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const worktree = WorktreeState.get(sessionDirectory)
|
const worktree = WorktreeState.get(sessionDirectory)
|
||||||
if (!worktree || worktree.status !== "pending") return true
|
if (!worktree || worktree.status !== "pending") return true
|
||||||
|
|
||||||
if (sessionDirectory === projectDirectory && session?.id) {
|
if (sessionDirectory === projectDirectory) {
|
||||||
sync.set("session_status", session?.id, { type: "busy" })
|
sync.set("session_status", session.id, { type: "busy" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if (sessionDirectory === projectDirectory && session?.id) {
|
if (sessionDirectory === projectDirectory) {
|
||||||
sync.set("session_status", session?.id, { type: "idle" })
|
sync.set("session_status", session.id, { type: "idle" })
|
||||||
}
|
}
|
||||||
removeOptimisticMessage()
|
removeOptimisticMessage()
|
||||||
for (const item of commentItems) {
|
for (const item of commentItems) {
|
||||||
@@ -1549,7 +1546,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
restoreInput()
|
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) => {
|
const abort = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||||
if (controller.signal.aborted) {
|
if (controller.signal.aborted) {
|
||||||
@@ -1577,7 +1574,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
if (timer.id === undefined) return
|
if (timer.id === undefined) return
|
||||||
clearTimeout(timer.id)
|
clearTimeout(timer.id)
|
||||||
})
|
})
|
||||||
pending.delete(session?.id || "")
|
pending.delete(session.id)
|
||||||
if (controller.signal.aborted) return false
|
if (controller.signal.aborted) return false
|
||||||
if (result.status === "failed") throw new Error(result.message)
|
if (result.status === "failed") throw new Error(result.message)
|
||||||
return true
|
return true
|
||||||
@@ -1587,7 +1584,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const ok = await waitForWorktree()
|
const ok = await waitForWorktree()
|
||||||
if (!ok) return
|
if (!ok) return
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
sessionID: session?.id || "",
|
sessionID: session.id,
|
||||||
agent,
|
agent,
|
||||||
model,
|
model,
|
||||||
messageID,
|
messageID,
|
||||||
@@ -1597,9 +1594,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void send().catch((err) => {
|
void send().catch((err) => {
|
||||||
pending.delete(session?.id || "")
|
pending.delete(session.id)
|
||||||
if (sessionDirectory === projectDirectory && session?.id) {
|
if (sessionDirectory === projectDirectory) {
|
||||||
sync.set("session_status", session?.id, { type: "idle" })
|
sync.set("session_status", session.id, { type: "idle" })
|
||||||
}
|
}
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("prompt.toast.promptSendFailed.title"),
|
title: language.t("prompt.toast.promptSendFailed.title"),
|
||||||
@@ -1621,28 +1618,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 (
|
return (
|
||||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-3">
|
<div class="relative size-full _max-h-[320px] flex flex-col gap-3">
|
||||||
<Show when={store.popover}>
|
<Show when={store.popover}>
|
||||||
@@ -1695,7 +1670,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">
|
<span class="text-14-regular text-text-strong whitespace-nowrap">
|
||||||
@{(item as { type: "agent"; name: string }).name}
|
@{(item as { type: "agent"; name: string }).name}
|
||||||
</span>
|
</span>
|
||||||
@@ -1760,9 +1735,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={store.dragging}>
|
<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">
|
<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>
|
<span class="text-14-regular">{language.t("prompt.dropzone.label")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1801,7 +1776,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1.5">
|
<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">
|
<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>
|
<span class="text-text-strong whitespace-nowrap">{getFilenameTruncated(item.path, 14)}</span>
|
||||||
<Show when={item.selection}>
|
<Show when={item.selection}>
|
||||||
@@ -1818,7 +1793,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
type="button"
|
type="button"
|
||||||
icon="close-small"
|
icon="close-small"
|
||||||
variant="ghost"
|
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) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (item.commentID) comments.remove(item.path, item.commentID)
|
if (item.commentID) comments.remove(item.path, item.commentID)
|
||||||
@@ -1848,7 +1823,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
when={attachment.mime.startsWith("image/")}
|
when={attachment.mime.startsWith("image/")}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="size-16 rounded-md bg-surface-base flex items-center justify-center border border-border-base">
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -1921,8 +1896,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative p-3 flex items-center justify-between">
|
<div class="relative p-3 flex items-center justify-between gap-2">
|
||||||
<div class="flex items-center justify-start gap-2">
|
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={store.mode === "shell"}>
|
<Match when={store.mode === "shell"}>
|
||||||
<div class="flex items-center gap-2 px-2 h-6">
|
<div class="flex items-center gap-2 px-2 h-6">
|
||||||
@@ -1934,6 +1909,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<Match when={store.mode === "normal"}>
|
<Match when={store.mode === "normal"}>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
|
gutter={8}
|
||||||
title={language.t("command.agent.cycle")}
|
title={language.t("command.agent.cycle")}
|
||||||
keybind={command.keybind("agent.cycle")}
|
keybind={command.keybind("agent.cycle")}
|
||||||
>
|
>
|
||||||
@@ -1941,9 +1917,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
options={local.agent.list().map((agent) => agent.name)}
|
options={local.agent.list().map((agent) => agent.name)}
|
||||||
current={local.agent.current()?.name ?? ""}
|
current={local.agent.current()?.name ?? ""}
|
||||||
onSelect={local.agent.set}
|
onSelect={local.agent.set}
|
||||||
class="capitalize"
|
class={`capitalize ${local.model.variant.list().length > 0 ? "max-w-[80px]" : "max-w-[120px]"}`}
|
||||||
|
valueClass="truncate"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
gutter={12}
|
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
<Show
|
<Show
|
||||||
@@ -1951,66 +1927,68 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
fallback={
|
fallback={
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
|
gutter={8}
|
||||||
title={language.t("command.model.choose")}
|
title={language.t("command.model.choose")}
|
||||||
keybind={command.keybind("model.choose")}
|
keybind={command.keybind("model.choose")}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
as="div"
|
as="div"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="px-2"
|
class="px-2 min-w-0 max-w-[240px]"
|
||||||
onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}
|
onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}
|
||||||
>
|
>
|
||||||
<Show when={local.model.current()?.provider?.id}>
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
||||||
</Show>
|
</Show>
|
||||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
<span class="truncate">
|
||||||
<MorphChevron
|
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||||
expanded={!!dialog.active?.id && dialog.active.id.startsWith("select-model-unpaid")}
|
</span>
|
||||||
/>
|
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
|
gutter={8}
|
||||||
title={language.t("command.model.choose")}
|
title={language.t("command.model.choose")}
|
||||||
keybind={command.keybind("model.choose")}
|
keybind={command.keybind("model.choose")}
|
||||||
>
|
>
|
||||||
<ModelSelectorPopover triggerAs={Button} triggerProps={{ variant: "ghost" }} gutter={12}>
|
<ModelSelectorPopover
|
||||||
{(open) => (
|
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 when={local.model.current()?.provider?.id}>
|
||||||
</Show>
|
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
||||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
</Show>
|
||||||
<MorphChevron expanded={open} class="text-text-weak" />
|
<span class="truncate">
|
||||||
</>
|
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||||
)}
|
</span>
|
||||||
|
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||||
</ModelSelectorPopover>
|
</ModelSelectorPopover>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={local.model.variant.list().length > 0}>
|
<Show when={local.model.variant.list().length > 0}>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
|
gutter={8}
|
||||||
title={language.t("command.model.variant.cycle")}
|
title={language.t("command.model.variant.cycle")}
|
||||||
keybind={command.keybind("model.variant.cycle")}
|
keybind={command.keybind("model.variant.cycle")}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
data-action="model-variant-cycle"
|
data-action="model-variant-cycle"
|
||||||
variant="ghost"
|
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()}
|
onClick={() => local.model.variant.cycle()}
|
||||||
>
|
>
|
||||||
<Show when={local.model.variant.list().length > 1}>
|
{local.model.variant.current() ?? language.t("common.default")}
|
||||||
<ReasoningIcon percentage={reasoningPercentage()} size={16} strokeWidth={1.25} />
|
|
||||||
</Show>
|
|
||||||
<CycleLabel value={currrentModelVariant()} />
|
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={permission.permissionsEnabled() && params.id}>
|
<Show when={permission.permissionsEnabled() && params.id}>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
|
gutter={8}
|
||||||
title={language.t("command.permissions.autoaccept.enable")}
|
title={language.t("command.permissions.autoaccept.enable")}
|
||||||
keybind={command.keybind("permissions.autoaccept")}
|
keybind={command.keybind("permissions.autoaccept")}
|
||||||
>
|
>
|
||||||
@@ -2018,7 +1996,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
|
onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
|
||||||
classList={{
|
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),
|
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
|
||||||
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
|
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
|
||||||
}}
|
}}
|
||||||
@@ -2040,7 +2018,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 absolute right-3 bottom-3">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
@@ -2052,19 +2030,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
e.currentTarget.value = ""
|
e.currentTarget.value = ""
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center gap-1.5 mr-1.5">
|
<div class="flex items-center gap-1 mr-1">
|
||||||
<SessionContextUsage />
|
<SessionContextUsage />
|
||||||
<Show when={store.mode === "normal"}>
|
<Show when={store.mode === "normal"}>
|
||||||
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
|
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="small"
|
class="size-6 px-1"
|
||||||
class="px-1"
|
|
||||||
onClick={() => fileInputRef.click()}
|
onClick={() => fileInputRef.click()}
|
||||||
aria-label={language.t("prompt.action.attachFile")}
|
aria-label={language.t("prompt.action.attachFile")}
|
||||||
>
|
>
|
||||||
<Icon name="photo" class="size-6 text-icon-base" />
|
<Icon name="photo" class="size-4.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -2083,7 +2060,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>{language.t("prompt.action.send")}</span>
|
<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>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -2091,10 +2068,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!prompt.dirty() && !working()}
|
disabled={!prompt.dirty() && !working() && commentCount() === 0}
|
||||||
icon={working() ? "stop" : "arrow-up"}
|
icon={working() ? "stop" : "arrow-up"}
|
||||||
variant="primary"
|
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")}
|
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const circle = () => (
|
const circle = () => (
|
||||||
<div class="p-1">
|
<div class="flex items-center justify-center">
|
||||||
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
|
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { Popover } from "@opencode-ai/ui/popover"
|
|||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
import { StatusPopover } from "../status-popover"
|
import { StatusPopover } from "../status-popover"
|
||||||
|
import { SessionOpenMenu } from "./session-open-menu"
|
||||||
|
|
||||||
export function SessionHeader() {
|
export function SessionHeader() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
@@ -117,7 +118,7 @@ export function SessionHeader() {
|
|||||||
function viewShare() {
|
function viewShare() {
|
||||||
const url = shareUrl()
|
const url = shareUrl()
|
||||||
if (!url) return
|
if (!url) return
|
||||||
platform.openLink(url)
|
void platform.openLink(url).catch(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
||||||
@@ -150,6 +151,7 @@ export function SessionHeader() {
|
|||||||
{(mount) => (
|
{(mount) => (
|
||||||
<Portal mount={mount()}>
|
<Portal mount={mount()}>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
<SessionOpenMenu dir={projectDirectory()} />
|
||||||
<StatusPopover />
|
<StatusPopover />
|
||||||
<Show when={showShare()}>
|
<Show when={showShare()}>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { createMemo, Show } from "solid-js"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useServer } from "@/context/server"
|
||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
|
import { FileTypeIcon } from "@opencode-ai/ui/file-type-icon"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
|
|
||||||
|
export function SessionOpenMenu(props: { dir: string }) {
|
||||||
|
const platform = usePlatform()
|
||||||
|
const server = useServer()
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
|
const enabled = createMemo(
|
||||||
|
() => platform.platform === "desktop" && platform.os === "macos" && server.isLocal() && !!props.dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
const open = (app?: string) => {
|
||||||
|
if (!props.dir) return
|
||||||
|
void platform.openLink(props.dir, app).catch((error) => {
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: error instanceof Error ? error.message : String(error),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
if (!props.dir) return
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(props.dir)
|
||||||
|
.then(() => {
|
||||||
|
showToast({
|
||||||
|
variant: "success",
|
||||||
|
icon: "check",
|
||||||
|
title: language.t("session.header.copyPath.copied"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("session.header.copyPath.copyFailed"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu modal={false}>
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
as={Button}
|
||||||
|
variant="ghost"
|
||||||
|
icon="folder"
|
||||||
|
class="rounded-sm h-[24px] py-1.5 pr-2 pl-2 gap-1.5 border-none shadow-none data-[expanded]:bg-surface-raised-base-active"
|
||||||
|
aria-label={language.t("session.header.open")}
|
||||||
|
>
|
||||||
|
<span class="text-12-regular text-text-strong">{language.t("session.header.open")}</span>
|
||||||
|
<Icon name="chevron-down" size="small" class="icon-base" />
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content class="mt-1 w-60">
|
||||||
|
<Show when={enabled()}>
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
|
||||||
|
<DropdownMenu.Item onSelect={() => open("Visual Studio Code")}>
|
||||||
|
<FileTypeIcon id="Vscode" class="size-5" />
|
||||||
|
<DropdownMenu.ItemLabel>VS Code</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onSelect={() => open("Cursor")}>
|
||||||
|
<FileTypeIcon id="Cursor" class="size-5" />
|
||||||
|
<DropdownMenu.ItemLabel>Cursor</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onSelect={() => open("Finder")}>
|
||||||
|
<Icon name="folder" size="small" class="icon-base shrink-0" />
|
||||||
|
<DropdownMenu.ItemLabel>Finder</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onSelect={() => open("Terminal")}>
|
||||||
|
<FileTypeIcon id="Console" class="size-5" />
|
||||||
|
<DropdownMenu.ItemLabel>Terminal</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onSelect={() => open("iTerm")}>
|
||||||
|
<FileTypeIcon id="Console" class="size-5" />
|
||||||
|
<DropdownMenu.ItemLabel>iTerm2</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onSelect={() => open("Ghostty")}>
|
||||||
|
<FileTypeIcon id="Console" class="size-5" />
|
||||||
|
<DropdownMenu.ItemLabel>Ghostty</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onSelect={() => open("Xcode")}>
|
||||||
|
<FileTypeIcon id="Swift" class="size-5" />
|
||||||
|
<DropdownMenu.ItemLabel>Xcode</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onSelect={() => open("Android Studio")}>
|
||||||
|
<FileTypeIcon id="Android" class="size-5" />
|
||||||
|
<DropdownMenu.ItemLabel>Android Studio</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
</Show>
|
||||||
|
<DropdownMenu.Item onSelect={copy}>
|
||||||
|
<Icon name="copy" size="small" class="icon-base shrink-0" />
|
||||||
|
<DropdownMenu.ItemLabel>{language.t("session.header.copyPath")}</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import { Select } from "@opencode-ai/ui/select"
|
|||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings, monoFontFamily } from "@/context/settings"
|
import { useSettings, monoFontFamily } from "@/context/settings"
|
||||||
@@ -131,12 +130,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
const soundOptions = [...SOUND_OPTIONS]
|
const soundOptions = [...SOUND_OPTIONS]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollFade
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
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="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<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">
|
<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>
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
|
||||||
@@ -232,7 +226,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
triggerVariant="settings"
|
triggerVariant="settings"
|
||||||
triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "field-sizing": "content" }}
|
triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "min-width": "180px" }}
|
||||||
>
|
>
|
||||||
{(option) => (
|
{(option) => (
|
||||||
<span style={{ "font-family": monoFontFamily(option?.value) }}>
|
<span style={{ "font-family": monoFontFamily(option?.value) }}>
|
||||||
@@ -417,7 +411,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
|
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -353,12 +352,7 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollFade
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
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="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<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 flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
@@ -436,6 +430,6 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</ScrollFade>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { type Component, For, Show } from "solid-js"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useModels } from "@/context/models"
|
import { useModels } from "@/context/models"
|
||||||
import { popularProviders } from "@/hooks/use-providers"
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
|
|
||||||
|
|
||||||
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
|
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
|
||||||
|
|
||||||
@@ -40,12 +39,7 @@ export const SettingsModels: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollFade
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
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="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<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 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>
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
||||||
@@ -131,6 +125,6 @@ export const SettingsModels: Component = () => {
|
|||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</ScrollFade>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { useGlobalSync } from "@/context/global-sync"
|
|||||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
import { DialogCustomProvider } from "./dialog-custom-provider"
|
||||||
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
|
|
||||||
|
|
||||||
type ProviderSource = "env" | "api" | "config" | "custom"
|
type ProviderSource = "env" | "api" | "config" | "custom"
|
||||||
type ProviderMeta = { source?: ProviderSource }
|
type ProviderMeta = { source?: ProviderSource }
|
||||||
@@ -116,12 +115,7 @@ export const SettingsProviders: Component = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollFade
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
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="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<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]">
|
<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>
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
||||||
@@ -232,11 +226,11 @@ export const SettingsProviders: Component = () => {
|
|||||||
</For>
|
</For>
|
||||||
|
|
||||||
<div
|
<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"
|
data-component="custom-provider-section"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col min-w-0">
|
<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" />
|
<ProviderIcon id={icon("synthetic")} class="size-5 shrink-0 icon-strong-base" />
|
||||||
<span class="text-14-medium text-text-strong">Custom provider</span>
|
<span class="text-14-medium text-text-strong">Custom provider</span>
|
||||||
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
||||||
@@ -267,6 +261,6 @@ export const SettingsProviders: Component = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollFade>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const once = { value: false }
|
const once = { value: false }
|
||||||
|
|
||||||
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
|
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) {
|
if (window.__OPENCODE__?.serverPassword) {
|
||||||
url.username = "opencode"
|
url.username = "opencode"
|
||||||
url.password = window.__OPENCODE__?.serverPassword
|
url.password = window.__OPENCODE__?.serverPassword
|
||||||
|
|||||||
@@ -137,7 +137,6 @@ export function Titlebar() {
|
|||||||
<header
|
<header
|
||||||
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center"
|
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center"
|
||||||
style={{ "min-height": minHeight() }}
|
style={{ "min-height": minHeight() }}
|
||||||
data-tauri-drag-region
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
@@ -145,10 +144,9 @@ export function Titlebar() {
|
|||||||
"pl-2": !mac(),
|
"pl-2": !mac(),
|
||||||
}}
|
}}
|
||||||
onMouseDown={drag}
|
onMouseDown={drag}
|
||||||
data-tauri-drag-region
|
|
||||||
>
|
>
|
||||||
<Show when={mac()}>
|
<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">
|
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="menu"
|
icon="menu"
|
||||||
@@ -222,13 +220,10 @@ export function Titlebar() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<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">
|
||||||
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 id="opencode-titlebar-center" class="pointer-events-auto w-full min-w-0 flex justify-center lg:w-fit" />
|
<div id="opencode-titlebar-center" class="pointer-events-auto w-full min-w-0 flex justify-center lg:w-fit" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -238,9 +233,8 @@ export function Titlebar() {
|
|||||||
"pr-6": !windows(),
|
"pr-6": !windows(),
|
||||||
}}
|
}}
|
||||||
onMouseDown={drag}
|
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()}>
|
<Show when={windows()}>
|
||||||
<div class="w-6 shrink-0" />
|
<div class="w-6 shrink-0" />
|
||||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ type ChildOptions = {
|
|||||||
bootstrap?: boolean
|
bootstrap?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||||
|
|
||||||
function normalizeProviderList(input: ProviderListResponse): ProviderListResponse {
|
function normalizeProviderList(input: ProviderListResponse): ProviderListResponse {
|
||||||
return {
|
return {
|
||||||
...input,
|
...input,
|
||||||
@@ -297,7 +299,7 @@ function createGlobalSync() {
|
|||||||
const aUpdated = sessionUpdatedAt(a)
|
const aUpdated = sessionUpdatedAt(a)
|
||||||
const bUpdated = sessionUpdatedAt(b)
|
const bUpdated = sessionUpdatedAt(b)
|
||||||
if (aUpdated !== bUpdated) return bUpdated - aUpdated
|
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) {
|
function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) {
|
||||||
@@ -325,7 +327,7 @@ function createGlobalSync() {
|
|||||||
const all = input
|
const all = input
|
||||||
.filter((s) => !!s?.id)
|
.filter((s) => !!s?.id)
|
||||||
.filter((s) => !s.time?.archived)
|
.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 roots = all.filter((s) => !s.parentID)
|
||||||
const children = all.filter((s) => !!s.parentID)
|
const children = all.filter((s) => !!s.parentID)
|
||||||
@@ -342,7 +344,7 @@ function createGlobalSync() {
|
|||||||
return sessionUpdatedAt(s) > cutoff
|
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) {
|
function ensureChild(directory: string) {
|
||||||
@@ -457,7 +459,7 @@ function createGlobalSync() {
|
|||||||
const nonArchived = (x.data ?? [])
|
const nonArchived = (x.data ?? [])
|
||||||
.filter((s) => !!s?.id)
|
.filter((s) => !!s?.id)
|
||||||
.filter((s) => !s.time?.archived)
|
.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
|
// Read the current limit at resolve-time so callers that bump the limit while
|
||||||
// a request is in-flight still get the expanded result.
|
// a request is in-flight still get the expanded result.
|
||||||
@@ -559,7 +561,7 @@ function createGlobalSync() {
|
|||||||
"permission",
|
"permission",
|
||||||
sessionID,
|
sessionID,
|
||||||
reconcile(
|
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" },
|
{ key: "id" },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -588,7 +590,7 @@ function createGlobalSync() {
|
|||||||
"question",
|
"question",
|
||||||
sessionID,
|
sessionID,
|
||||||
reconcile(
|
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" },
|
{ key: "id" },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -986,7 +988,7 @@ function createGlobalSync() {
|
|||||||
.filter((p) => !!p?.id)
|
.filter((p) => !!p?.id)
|
||||||
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
.sort((a, b) => cmp(a.id, b.id))
|
||||||
setGlobalStore("project", projects)
|
setGlobalStore("project", projects)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -682,12 +682,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
if (!current) return
|
if (!current) return
|
||||||
|
|
||||||
const all = current.all.filter((x) => x !== tab)
|
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(() => {
|
batch(() => {
|
||||||
setStore("sessionTabs", session, "all", all)
|
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)
|
setStore("sessionTabs", session, "active", next)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export type Platform = {
|
|||||||
/** App version */
|
/** App version */
|
||||||
version?: string
|
version?: string
|
||||||
|
|
||||||
/** Open a URL in the default browser */
|
/** Open a URL/path using the OS (optionally with a specific app) */
|
||||||
openLink(url: string): void
|
openLink(url: string, openWith?: string): Promise<void>
|
||||||
|
|
||||||
/** Restart the app */
|
/** Restart the app */
|
||||||
restart(): Promise<void>
|
restart(): Promise<void>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
|||||||
|
|
||||||
const keyFor = (directory: string, id: string) => `${directory}\n${id}`
|
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({
|
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||||
name: "Sync",
|
name: "Sync",
|
||||||
init: () => {
|
init: () => {
|
||||||
@@ -59,7 +61,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const next = items
|
const next = items
|
||||||
.map((x) => x.info)
|
.map((x) => x.info)
|
||||||
.filter((m) => !!m?.id)
|
.filter((m) => !!m?.id)
|
||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
.sort((a, b) => cmp(a.id, b.id))
|
||||||
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
input.setStore("message", input.sessionID, reconcile(next, { key: "id" }))
|
input.setStore("message", input.sessionID, reconcile(next, { key: "id" }))
|
||||||
@@ -69,7 +71,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
"part",
|
"part",
|
||||||
message.info.id,
|
message.info.id,
|
||||||
reconcile(
|
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" },
|
{ key: "id" },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -129,7 +131,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
||||||
messages.splice(result.index, 0, message)
|
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) => {
|
await client.session.list().then((x) => {
|
||||||
const sessions = (x.data ?? [])
|
const sessions = (x.data ?? [])
|
||||||
.filter((s) => !!s?.id)
|
.filter((s) => !!s?.id)
|
||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
.sort((a, b) => cmp(a.id, b.id))
|
||||||
.slice(0, store.limit)
|
.slice(0, store.limit)
|
||||||
setStore("session", reconcile(sessions, { key: "id" }))
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
|||||||
const platform: Platform = {
|
const platform: Platform = {
|
||||||
platform: "web",
|
platform: "web",
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
openLink(url: string) {
|
async openLink(url: string, _openWith?: string) {
|
||||||
window.open(url, "_blank")
|
window.open(url, "_blank")
|
||||||
},
|
},
|
||||||
back() {
|
back() {
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const dict = {
|
|||||||
|
|
||||||
"command.session.new": "جلسة جديدة",
|
"command.session.new": "جلسة جديدة",
|
||||||
"command.file.open": "فتح ملف",
|
"command.file.open": "فتح ملف",
|
||||||
"command.file.open.description": "البحث في الملفات والأوامر",
|
|
||||||
"command.context.addSelection": "إضافة التحديد إلى السياق",
|
"command.context.addSelection": "إضافة التحديد إلى السياق",
|
||||||
"command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي",
|
"command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي",
|
||||||
"command.terminal.toggle": "تبديل المحطة الطرفية",
|
"command.terminal.toggle": "تبديل المحطة الطرفية",
|
||||||
@@ -70,6 +69,7 @@ export const dict = {
|
|||||||
"command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
|
"command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
|
||||||
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
|
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
|
||||||
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
|
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
|
||||||
|
"command.workspace.toggle": "تبديل مساحات العمل",
|
||||||
"command.session.undo": "تراجع",
|
"command.session.undo": "تراجع",
|
||||||
"command.session.undo.description": "تراجع عن الرسالة الأخيرة",
|
"command.session.undo.description": "تراجع عن الرسالة الأخيرة",
|
||||||
"command.session.redo": "إعادة",
|
"command.session.redo": "إعادة",
|
||||||
@@ -83,7 +83,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "إلغاء مشاركة الجلسة",
|
"command.session.unshare": "إلغاء مشاركة الجلسة",
|
||||||
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
|
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
|
||||||
|
|
||||||
"palette.search.placeholder": "البحث في الملفات والأوامر",
|
"palette.search.placeholder": "البحث في الملفات والأوامر والجلسات",
|
||||||
"palette.empty": "لا توجد نتائج",
|
"palette.empty": "لا توجد نتائج",
|
||||||
"palette.group.commands": "الأوامر",
|
"palette.group.commands": "الأوامر",
|
||||||
"palette.group.files": "الملفات",
|
"palette.group.files": "الملفات",
|
||||||
@@ -348,6 +348,11 @@ export const dict = {
|
|||||||
"toast.permissions.autoaccept.off.title": "توقف قبول التعديلات تلقائيًا",
|
"toast.permissions.autoaccept.off.title": "توقف قبول التعديلات تلقائيًا",
|
||||||
"toast.permissions.autoaccept.off.description": "ستتطلب أذونات التحرير والكتابة موافقة",
|
"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.title": "لم يتم تحديد نموذج",
|
||||||
"toast.model.none.description": "قم بتوصيل موفر لتلخيص هذه الجلسة",
|
"toast.model.none.description": "قم بتوصيل موفر لتلخيص هذه الجلسة",
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const dict = {
|
|||||||
|
|
||||||
"command.session.new": "Nova sessão",
|
"command.session.new": "Nova sessão",
|
||||||
"command.file.open": "Abrir arquivo",
|
"command.file.open": "Abrir arquivo",
|
||||||
"command.file.open.description": "Buscar arquivos e comandos",
|
|
||||||
"command.context.addSelection": "Adicionar seleção ao contexto",
|
"command.context.addSelection": "Adicionar seleção ao contexto",
|
||||||
"command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual",
|
"command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual",
|
||||||
"command.terminal.toggle": "Alternar terminal",
|
"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.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.enable": "Aceitar edições automaticamente",
|
||||||
"command.permissions.autoaccept.disable": "Parar de 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": "Desfazer",
|
||||||
"command.session.undo.description": "Desfazer a última mensagem",
|
"command.session.undo.description": "Desfazer a última mensagem",
|
||||||
"command.session.redo": "Refazer",
|
"command.session.redo": "Refazer",
|
||||||
@@ -83,7 +83,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Parar de compartilhar sessão",
|
"command.session.unshare": "Parar de compartilhar sessão",
|
||||||
"command.session.unshare.description": "Parar de compartilhar esta 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.empty": "Nenhum resultado encontrado",
|
||||||
"palette.group.commands": "Comandos",
|
"palette.group.commands": "Comandos",
|
||||||
"palette.group.files": "Arquivos",
|
"palette.group.files": "Arquivos",
|
||||||
@@ -347,6 +347,11 @@ export const dict = {
|
|||||||
"toast.permissions.autoaccept.off.title": "Parou de aceitar edições automaticamente",
|
"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.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.title": "Nenhum modelo selecionado",
|
||||||
"toast.model.none.description": "Conecte um provedor para resumir esta sessão",
|
"toast.model.none.description": "Conecte um provedor para resumir esta sessão",
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const dict = {
|
|||||||
|
|
||||||
"command.session.new": "Ny session",
|
"command.session.new": "Ny session",
|
||||||
"command.file.open": "Åbn fil",
|
"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": "Tilføj markering til kontekst",
|
||||||
"command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil",
|
"command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil",
|
||||||
"command.terminal.toggle": "Skift terminal",
|
"command.terminal.toggle": "Skift terminal",
|
||||||
@@ -70,6 +69,7 @@ export const dict = {
|
|||||||
"command.model.variant.cycle.description": "Skift til næste indsatsniveau",
|
"command.model.variant.cycle.description": "Skift til næste indsatsniveau",
|
||||||
"command.permissions.autoaccept.enable": "Accepter ændringer automatisk",
|
"command.permissions.autoaccept.enable": "Accepter ændringer automatisk",
|
||||||
"command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer",
|
"command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer",
|
||||||
|
"command.workspace.toggle": "Skift arbejdsområder",
|
||||||
"command.session.undo": "Fortryd",
|
"command.session.undo": "Fortryd",
|
||||||
"command.session.undo.description": "Fortryd den sidste besked",
|
"command.session.undo.description": "Fortryd den sidste besked",
|
||||||
"command.session.redo": "Omgør",
|
"command.session.redo": "Omgør",
|
||||||
@@ -83,7 +83,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Stop deling af session",
|
"command.session.unshare": "Stop deling af session",
|
||||||
"command.session.unshare.description": "Stop med at dele denne 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.empty": "Ingen resultater fundet",
|
||||||
"palette.group.commands": "Kommandoer",
|
"palette.group.commands": "Kommandoer",
|
||||||
"palette.group.files": "Filer",
|
"palette.group.files": "Filer",
|
||||||
@@ -349,6 +349,11 @@ export const dict = {
|
|||||||
"toast.permissions.autoaccept.off.title": "Stoppede automatisk accept af ændringer",
|
"toast.permissions.autoaccept.off.title": "Stoppede automatisk accept af ændringer",
|
||||||
"toast.permissions.autoaccept.off.description": "Redigerings- og skrivetilladelser vil kræve godkendelse",
|
"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.title": "Ingen model valgt",
|
||||||
"toast.model.none.description": "Forbind en udbyder for at opsummere denne session",
|
"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.session.new": "Neue Sitzung",
|
||||||
"command.file.open": "Datei öffnen",
|
"command.file.open": "Datei öffnen",
|
||||||
"command.file.open.description": "Dateien und Befehle durchsuchen",
|
|
||||||
"command.context.addSelection": "Auswahl zum Kontext hinzufügen",
|
"command.context.addSelection": "Auswahl zum Kontext hinzufügen",
|
||||||
"command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen",
|
"command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen",
|
||||||
"command.terminal.toggle": "Terminal umschalten",
|
"command.terminal.toggle": "Terminal umschalten",
|
||||||
@@ -74,6 +73,7 @@ export const dict = {
|
|||||||
"command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln",
|
"command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln",
|
||||||
"command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren",
|
"command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren",
|
||||||
"command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen",
|
"command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen",
|
||||||
|
"command.workspace.toggle": "Arbeitsbereiche umschalten",
|
||||||
"command.session.undo": "Rückgängig",
|
"command.session.undo": "Rückgängig",
|
||||||
"command.session.undo.description": "Letzte Nachricht rückgängig machen",
|
"command.session.undo.description": "Letzte Nachricht rückgängig machen",
|
||||||
"command.session.redo": "Wiederherstellen",
|
"command.session.redo": "Wiederherstellen",
|
||||||
@@ -87,7 +87,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Teilen der Sitzung aufheben",
|
"command.session.unshare": "Teilen der Sitzung aufheben",
|
||||||
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
|
"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.empty": "Keine Ergebnisse gefunden",
|
||||||
"palette.group.commands": "Befehle",
|
"palette.group.commands": "Befehle",
|
||||||
"palette.group.files": "Dateien",
|
"palette.group.files": "Dateien",
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const dict = {
|
|||||||
|
|
||||||
"command.session.new": "New session",
|
"command.session.new": "New session",
|
||||||
"command.file.open": "Open file",
|
"command.file.open": "Open file",
|
||||||
"command.file.open.description": "Search files and commands",
|
|
||||||
"command.tab.close": "Close tab",
|
"command.tab.close": "Close tab",
|
||||||
"command.context.addSelection": "Add selection to context",
|
"command.context.addSelection": "Add selection to context",
|
||||||
"command.context.addSelection.description": "Add selected lines from the current file",
|
"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.model.variant.cycle.description": "Switch to the next effort level",
|
||||||
"command.permissions.autoaccept.enable": "Auto-accept edits",
|
"command.permissions.autoaccept.enable": "Auto-accept edits",
|
||||||
"command.permissions.autoaccept.disable": "Stop auto-accepting 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": "Undo",
|
||||||
"command.session.undo.description": "Undo the last message",
|
"command.session.undo.description": "Undo the last message",
|
||||||
"command.session.redo": "Redo",
|
"command.session.redo": "Redo",
|
||||||
@@ -84,7 +85,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Unshare session",
|
"command.session.unshare": "Unshare session",
|
||||||
"command.session.unshare.description": "Stop sharing this 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.empty": "No results found",
|
||||||
"palette.group.commands": "Commands",
|
"palette.group.commands": "Commands",
|
||||||
"palette.group.files": "Files",
|
"palette.group.files": "Files",
|
||||||
@@ -350,6 +351,11 @@ export const dict = {
|
|||||||
"toast.theme.title": "Theme switched",
|
"toast.theme.title": "Theme switched",
|
||||||
"toast.scheme.title": "Color scheme",
|
"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.title": "Auto-accepting edits",
|
||||||
"toast.permissions.autoaccept.on.description": "Edit and write permissions will be automatically approved",
|
"toast.permissions.autoaccept.on.description": "Edit and write permissions will be automatically approved",
|
||||||
"toast.permissions.autoaccept.off.title": "Stopped auto-accepting edits",
|
"toast.permissions.autoaccept.off.title": "Stopped auto-accepting edits",
|
||||||
@@ -463,6 +469,11 @@ export const dict = {
|
|||||||
|
|
||||||
"session.header.search.placeholder": "Search {{project}}",
|
"session.header.search.placeholder": "Search {{project}}",
|
||||||
"session.header.searchFiles": "Search files",
|
"session.header.searchFiles": "Search files",
|
||||||
|
"session.header.open": "Open",
|
||||||
|
"session.header.openIn": "Open in",
|
||||||
|
"session.header.copyPath": "Copy Path",
|
||||||
|
"session.header.copyPath.copied": "Copied path",
|
||||||
|
"session.header.copyPath.copyFailed": "Failed to copy path to clipboard",
|
||||||
|
|
||||||
"status.popover.trigger": "Status",
|
"status.popover.trigger": "Status",
|
||||||
"status.popover.ariaLabel": "Server configurations",
|
"status.popover.ariaLabel": "Server configurations",
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const dict = {
|
|||||||
|
|
||||||
"command.session.new": "Nueva sesión",
|
"command.session.new": "Nueva sesión",
|
||||||
"command.file.open": "Abrir archivo",
|
"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": "Añadir selección al contexto",
|
||||||
"command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual",
|
"command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual",
|
||||||
"command.terminal.toggle": "Alternar terminal",
|
"command.terminal.toggle": "Alternar terminal",
|
||||||
@@ -70,6 +69,7 @@ export const dict = {
|
|||||||
"command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo",
|
"command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo",
|
||||||
"command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente",
|
"command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente",
|
||||||
"command.permissions.autoaccept.disable": "Dejar de 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": "Deshacer",
|
||||||
"command.session.undo.description": "Deshacer el último mensaje",
|
"command.session.undo.description": "Deshacer el último mensaje",
|
||||||
"command.session.redo": "Rehacer",
|
"command.session.redo": "Rehacer",
|
||||||
@@ -83,7 +83,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Dejar de compartir sesión",
|
"command.session.unshare": "Dejar de compartir sesión",
|
||||||
"command.session.unshare.description": "Dejar de compartir esta 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.empty": "No se encontraron resultados",
|
||||||
"palette.group.commands": "Comandos",
|
"palette.group.commands": "Comandos",
|
||||||
"palette.group.files": "Archivos",
|
"palette.group.files": "Archivos",
|
||||||
@@ -350,6 +350,11 @@ export const dict = {
|
|||||||
"toast.permissions.autoaccept.off.title": "Se dejó de aceptar ediciones automáticamente",
|
"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.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.title": "Ningún modelo seleccionado",
|
||||||
"toast.model.none.description": "Conecta un proveedor para resumir esta sesión",
|
"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.session.new": "Nouvelle session",
|
||||||
"command.file.open": "Ouvrir un fichier",
|
"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": "Ajouter la sélection au contexte",
|
||||||
"command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel",
|
"command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel",
|
||||||
"command.terminal.toggle": "Basculer le terminal",
|
"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.model.variant.cycle.description": "Passer au niveau d'effort suivant",
|
||||||
"command.permissions.autoaccept.enable": "Accepter automatiquement les modifications",
|
"command.permissions.autoaccept.enable": "Accepter automatiquement les modifications",
|
||||||
"command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des 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": "Annuler",
|
||||||
"command.session.undo.description": "Annuler le dernier message",
|
"command.session.undo.description": "Annuler le dernier message",
|
||||||
"command.session.redo": "Rétablir",
|
"command.session.redo": "Rétablir",
|
||||||
@@ -83,7 +83,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Ne plus partager la session",
|
"command.session.unshare": "Ne plus partager la session",
|
||||||
"command.session.unshare.description": "Arrêter de partager cette 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.empty": "Aucun résultat trouvé",
|
||||||
"palette.group.commands": "Commandes",
|
"palette.group.commands": "Commandes",
|
||||||
"palette.group.files": "Fichiers",
|
"palette.group.files": "Fichiers",
|
||||||
@@ -352,6 +352,11 @@ export const dict = {
|
|||||||
"toast.permissions.autoaccept.off.description":
|
"toast.permissions.autoaccept.off.description":
|
||||||
"Les permissions de modification et d'écriture nécessiteront une approbation",
|
"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.title": "Aucun modèle sélectionné",
|
||||||
"toast.model.none.description": "Connectez un fournisseur pour résumer cette session",
|
"toast.model.none.description": "Connectez un fournisseur pour résumer cette session",
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const dict = {
|
|||||||
|
|
||||||
"command.session.new": "新しいセッション",
|
"command.session.new": "新しいセッション",
|
||||||
"command.file.open": "ファイルを開く",
|
"command.file.open": "ファイルを開く",
|
||||||
"command.file.open.description": "ファイルとコマンドを検索",
|
|
||||||
"command.context.addSelection": "選択範囲をコンテキストに追加",
|
"command.context.addSelection": "選択範囲をコンテキストに追加",
|
||||||
"command.context.addSelection.description": "現在のファイルから選択した行を追加",
|
"command.context.addSelection.description": "現在のファイルから選択した行を追加",
|
||||||
"command.terminal.toggle": "ターミナルの切り替え",
|
"command.terminal.toggle": "ターミナルの切り替え",
|
||||||
@@ -70,6 +69,7 @@ export const dict = {
|
|||||||
"command.model.variant.cycle.description": "次の思考レベルに切り替え",
|
"command.model.variant.cycle.description": "次の思考レベルに切り替え",
|
||||||
"command.permissions.autoaccept.enable": "編集を自動承認",
|
"command.permissions.autoaccept.enable": "編集を自動承認",
|
||||||
"command.permissions.autoaccept.disable": "編集の自動承認を停止",
|
"command.permissions.autoaccept.disable": "編集の自動承認を停止",
|
||||||
|
"command.workspace.toggle": "ワークスペースを切り替え",
|
||||||
"command.session.undo": "元に戻す",
|
"command.session.undo": "元に戻す",
|
||||||
"command.session.undo.description": "最後のメッセージを元に戻す",
|
"command.session.undo.description": "最後のメッセージを元に戻す",
|
||||||
"command.session.redo": "やり直す",
|
"command.session.redo": "やり直す",
|
||||||
@@ -83,7 +83,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "セッションの共有を停止",
|
"command.session.unshare": "セッションの共有を停止",
|
||||||
"command.session.unshare.description": "このセッションの共有を停止",
|
"command.session.unshare.description": "このセッションの共有を停止",
|
||||||
|
|
||||||
"palette.search.placeholder": "ファイルとコマンドを検索",
|
"palette.search.placeholder": "ファイル、コマンド、セッションを検索",
|
||||||
"palette.empty": "結果が見つかりません",
|
"palette.empty": "結果が見つかりません",
|
||||||
"palette.group.commands": "コマンド",
|
"palette.group.commands": "コマンド",
|
||||||
"palette.group.files": "ファイル",
|
"palette.group.files": "ファイル",
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export const dict = {
|
|||||||
|
|
||||||
"command.session.new": "새 세션",
|
"command.session.new": "새 세션",
|
||||||
"command.file.open": "파일 열기",
|
"command.file.open": "파일 열기",
|
||||||
"command.file.open.description": "파일 및 명령어 검색",
|
|
||||||
"command.context.addSelection": "선택 영역을 컨텍스트에 추가",
|
"command.context.addSelection": "선택 영역을 컨텍스트에 추가",
|
||||||
"command.context.addSelection.description": "현재 파일에서 선택한 줄을 추가",
|
"command.context.addSelection.description": "현재 파일에서 선택한 줄을 추가",
|
||||||
"command.terminal.toggle": "터미널 토글",
|
"command.terminal.toggle": "터미널 토글",
|
||||||
@@ -74,6 +73,7 @@ export const dict = {
|
|||||||
"command.model.variant.cycle.description": "다음 생각 수준으로 전환",
|
"command.model.variant.cycle.description": "다음 생각 수준으로 전환",
|
||||||
"command.permissions.autoaccept.enable": "편집 자동 수락",
|
"command.permissions.autoaccept.enable": "편집 자동 수락",
|
||||||
"command.permissions.autoaccept.disable": "편집 자동 수락 중지",
|
"command.permissions.autoaccept.disable": "편집 자동 수락 중지",
|
||||||
|
"command.workspace.toggle": "작업 공간 전환",
|
||||||
"command.session.undo": "실행 취소",
|
"command.session.undo": "실행 취소",
|
||||||
"command.session.undo.description": "마지막 메시지 실행 취소",
|
"command.session.undo.description": "마지막 메시지 실행 취소",
|
||||||
"command.session.redo": "다시 실행",
|
"command.session.redo": "다시 실행",
|
||||||
@@ -87,7 +87,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "세션 공유 중지",
|
"command.session.unshare": "세션 공유 중지",
|
||||||
"command.session.unshare.description": "이 세션 공유 중지",
|
"command.session.unshare.description": "이 세션 공유 중지",
|
||||||
|
|
||||||
"palette.search.placeholder": "파일 및 명령어 검색",
|
"palette.search.placeholder": "파일, 명령어 및 세션 검색",
|
||||||
"palette.empty": "결과 없음",
|
"palette.empty": "결과 없음",
|
||||||
"palette.group.commands": "명령어",
|
"palette.group.commands": "명령어",
|
||||||
"palette.group.files": "파일",
|
"palette.group.files": "파일",
|
||||||
@@ -351,6 +351,11 @@ export const dict = {
|
|||||||
"toast.permissions.autoaccept.off.title": "편집 자동 수락 중지됨",
|
"toast.permissions.autoaccept.off.title": "편집 자동 수락 중지됨",
|
||||||
"toast.permissions.autoaccept.off.description": "편집 및 쓰기 권한 승인이 필요합니다",
|
"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.title": "선택된 모델 없음",
|
||||||
"toast.model.none.description": "이 세션을 요약하려면 공급자를 연결하세요",
|
"toast.model.none.description": "이 세션을 요약하려면 공급자를 연결하세요",
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export const dict = {
|
|||||||
|
|
||||||
"command.session.new": "Ny sesjon",
|
"command.session.new": "Ny sesjon",
|
||||||
"command.file.open": "Åpne fil",
|
"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": "Legg til markering i kontekst",
|
||||||
"command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil",
|
"command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil",
|
||||||
"command.terminal.toggle": "Veksle terminal",
|
"command.terminal.toggle": "Veksle terminal",
|
||||||
@@ -73,6 +72,7 @@ export const dict = {
|
|||||||
"command.model.variant.cycle.description": "Bytt til neste innsatsnivå",
|
"command.model.variant.cycle.description": "Bytt til neste innsatsnivå",
|
||||||
"command.permissions.autoaccept.enable": "Godta endringer automatisk",
|
"command.permissions.autoaccept.enable": "Godta endringer automatisk",
|
||||||
"command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk",
|
"command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk",
|
||||||
|
"command.workspace.toggle": "Veksle arbeidsområder",
|
||||||
"command.session.undo": "Angre",
|
"command.session.undo": "Angre",
|
||||||
"command.session.undo.description": "Angre siste melding",
|
"command.session.undo.description": "Angre siste melding",
|
||||||
"command.session.redo": "Gjør om",
|
"command.session.redo": "Gjør om",
|
||||||
@@ -86,7 +86,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Slutt å dele sesjon",
|
"command.session.unshare": "Slutt å dele sesjon",
|
||||||
"command.session.unshare.description": "Slutt å dele denne sesjonen",
|
"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.empty": "Ingen resultater funnet",
|
||||||
"palette.group.commands": "Kommandoer",
|
"palette.group.commands": "Kommandoer",
|
||||||
"palette.group.files": "Filer",
|
"palette.group.files": "Filer",
|
||||||
@@ -351,6 +351,11 @@ export const dict = {
|
|||||||
"toast.permissions.autoaccept.off.title": "Sluttet å godta endringer automatisk",
|
"toast.permissions.autoaccept.off.title": "Sluttet å godta endringer automatisk",
|
||||||
"toast.permissions.autoaccept.off.description": "Redigerings- og skrivetillatelser vil kreve godkjenning",
|
"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.title": "Ingen modell valgt",
|
||||||
"toast.model.none.description": "Koble til en leverandør for å oppsummere denne sesjonen",
|
"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.session.new": "Nowa sesja",
|
||||||
"command.file.open": "Otwórz plik",
|
"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": "Dodaj zaznaczenie do kontekstu",
|
||||||
"command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku",
|
"command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku",
|
||||||
"command.terminal.toggle": "Przełącz terminal",
|
"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.model.variant.cycle.description": "Przełącz na następny poziom wysiłku",
|
||||||
"command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji",
|
"command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji",
|
||||||
"command.permissions.autoaccept.disable": "Zatrzymaj 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": "Cofnij",
|
||||||
"command.session.undo.description": "Cofnij ostatnią wiadomość",
|
"command.session.undo.description": "Cofnij ostatnią wiadomość",
|
||||||
"command.session.redo": "Ponów",
|
"command.session.redo": "Ponów",
|
||||||
@@ -83,7 +83,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Przestań udostępniać sesję",
|
"command.session.unshare": "Przestań udostępniać sesję",
|
||||||
"command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji",
|
"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.empty": "Brak wyników",
|
||||||
"palette.group.commands": "Polecenia",
|
"palette.group.commands": "Polecenia",
|
||||||
"palette.group.files": "Pliki",
|
"palette.group.files": "Pliki",
|
||||||
@@ -349,6 +349,11 @@ export const dict = {
|
|||||||
"toast.permissions.autoaccept.off.title": "Zatrzymano automatyczne akceptowanie edycji",
|
"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.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.title": "Nie wybrano modelu",
|
||||||
"toast.model.none.description": "Połącz dostawcę, aby podsumować tę sesję",
|
"toast.model.none.description": "Połącz dostawcę, aby podsumować tę sesję",
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const dict = {
|
|||||||
|
|
||||||
"command.session.new": "Новая сессия",
|
"command.session.new": "Новая сессия",
|
||||||
"command.file.open": "Открыть файл",
|
"command.file.open": "Открыть файл",
|
||||||
"command.file.open.description": "Поиск файлов и команд",
|
|
||||||
"command.context.addSelection": "Добавить выделение в контекст",
|
"command.context.addSelection": "Добавить выделение в контекст",
|
||||||
"command.context.addSelection.description": "Добавить выбранные строки из текущего файла",
|
"command.context.addSelection.description": "Добавить выбранные строки из текущего файла",
|
||||||
"command.terminal.toggle": "Переключить терминал",
|
"command.terminal.toggle": "Переключить терминал",
|
||||||
@@ -70,6 +69,7 @@ export const dict = {
|
|||||||
"command.model.variant.cycle.description": "Переключиться к следующему уровню усилий",
|
"command.model.variant.cycle.description": "Переключиться к следующему уровню усилий",
|
||||||
"command.permissions.autoaccept.enable": "Авто-принятие изменений",
|
"command.permissions.autoaccept.enable": "Авто-принятие изменений",
|
||||||
"command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений",
|
"command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений",
|
||||||
|
"command.workspace.toggle": "Переключить рабочие пространства",
|
||||||
"command.session.undo": "Отменить",
|
"command.session.undo": "Отменить",
|
||||||
"command.session.undo.description": "Отменить последнее сообщение",
|
"command.session.undo.description": "Отменить последнее сообщение",
|
||||||
"command.session.redo": "Повторить",
|
"command.session.redo": "Повторить",
|
||||||
@@ -83,7 +83,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Отменить публикацию",
|
"command.session.unshare": "Отменить публикацию",
|
||||||
"command.session.unshare.description": "Прекратить публикацию сессии",
|
"command.session.unshare.description": "Прекратить публикацию сессии",
|
||||||
|
|
||||||
"palette.search.placeholder": "Поиск файлов и команд",
|
"palette.search.placeholder": "Поиск файлов, команд и сессий",
|
||||||
"palette.empty": "Ничего не найдено",
|
"palette.empty": "Ничего не найдено",
|
||||||
"palette.group.commands": "Команды",
|
"palette.group.commands": "Команды",
|
||||||
"palette.group.files": "Файлы",
|
"palette.group.files": "Файлы",
|
||||||
@@ -350,6 +350,11 @@ export const dict = {
|
|||||||
"toast.permissions.autoaccept.off.title": "Авто-принятие остановлено",
|
"toast.permissions.autoaccept.off.title": "Авто-принятие остановлено",
|
||||||
"toast.permissions.autoaccept.off.description": "Редактирование и запись потребуют подтверждения",
|
"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.title": "Модель не выбрана",
|
||||||
"toast.model.none.description": "Подключите провайдера для суммаризации сессии",
|
"toast.model.none.description": "Подключите провайдера для суммаризации сессии",
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const dict = {
|
|||||||
|
|
||||||
"command.session.new": "เซสชันใหม่",
|
"command.session.new": "เซสชันใหม่",
|
||||||
"command.file.open": "เปิดไฟล์",
|
"command.file.open": "เปิดไฟล์",
|
||||||
"command.file.open.description": "ค้นหาไฟล์และคำสั่ง",
|
|
||||||
"command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท",
|
"command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท",
|
||||||
"command.context.addSelection.description": "เพิ่มบรรทัดที่เลือกจากไฟล์ปัจจุบัน",
|
"command.context.addSelection.description": "เพิ่มบรรทัดที่เลือกจากไฟล์ปัจจุบัน",
|
||||||
"command.terminal.toggle": "สลับเทอร์มินัล",
|
"command.terminal.toggle": "สลับเทอร์มินัล",
|
||||||
@@ -70,6 +69,7 @@ export const dict = {
|
|||||||
"command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
|
"command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
|
||||||
"command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
|
"command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||||
"command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
"command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||||
|
"command.workspace.toggle": "สลับพื้นที่ทำงาน",
|
||||||
"command.session.undo": "ยกเลิก",
|
"command.session.undo": "ยกเลิก",
|
||||||
"command.session.undo.description": "ยกเลิกข้อความล่าสุด",
|
"command.session.undo.description": "ยกเลิกข้อความล่าสุด",
|
||||||
"command.session.redo": "ทำซ้ำ",
|
"command.session.redo": "ทำซ้ำ",
|
||||||
@@ -83,7 +83,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "ยกเลิกการแชร์เซสชัน",
|
"command.session.unshare": "ยกเลิกการแชร์เซสชัน",
|
||||||
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
|
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
|
||||||
|
|
||||||
"palette.search.placeholder": "ค้นหาไฟล์และคำสั่ง",
|
"palette.search.placeholder": "ค้นหาไฟล์ คำสั่ง และเซสชัน",
|
||||||
"palette.empty": "ไม่พบผลลัพธ์",
|
"palette.empty": "ไม่พบผลลัพธ์",
|
||||||
"palette.group.commands": "คำสั่ง",
|
"palette.group.commands": "คำสั่ง",
|
||||||
"palette.group.files": "ไฟล์",
|
"palette.group.files": "ไฟล์",
|
||||||
@@ -349,10 +349,15 @@ export const dict = {
|
|||||||
"toast.scheme.title": "โทนสี",
|
"toast.scheme.title": "โทนสี",
|
||||||
|
|
||||||
"toast.permissions.autoaccept.on.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.title": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||||
"toast.permissions.autoaccept.off.description": "สิทธิ์การแก้ไขและเขียนจะต้องได้รับการอนุมัติ",
|
"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.title": "ไม่ได้เลือกโมเดล",
|
||||||
"toast.model.none.description": "เชื่อมต่อผู้ให้บริการเพื่อสรุปเซสชันนี้",
|
"toast.model.none.description": "เชื่อมต่อผู้ให้บริการเพื่อสรุปเซสชันนี้",
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export const dict = {
|
|||||||
|
|
||||||
"command.session.new": "新建会话",
|
"command.session.new": "新建会话",
|
||||||
"command.file.open": "打开文件",
|
"command.file.open": "打开文件",
|
||||||
"command.file.open.description": "搜索文件和命令",
|
|
||||||
"command.context.addSelection": "将所选内容添加到上下文",
|
"command.context.addSelection": "将所选内容添加到上下文",
|
||||||
"command.context.addSelection.description": "添加当前文件中选中的行",
|
"command.context.addSelection.description": "添加当前文件中选中的行",
|
||||||
"command.terminal.toggle": "切换终端",
|
"command.terminal.toggle": "切换终端",
|
||||||
@@ -74,6 +73,7 @@ export const dict = {
|
|||||||
"command.model.variant.cycle.description": "切换到下一个强度等级",
|
"command.model.variant.cycle.description": "切换到下一个强度等级",
|
||||||
"command.permissions.autoaccept.enable": "自动接受编辑",
|
"command.permissions.autoaccept.enable": "自动接受编辑",
|
||||||
"command.permissions.autoaccept.disable": "停止自动接受编辑",
|
"command.permissions.autoaccept.disable": "停止自动接受编辑",
|
||||||
|
"command.workspace.toggle": "切换工作区",
|
||||||
"command.session.undo": "撤销",
|
"command.session.undo": "撤销",
|
||||||
"command.session.undo.description": "撤销上一条消息",
|
"command.session.undo.description": "撤销上一条消息",
|
||||||
"command.session.redo": "重做",
|
"command.session.redo": "重做",
|
||||||
@@ -87,7 +87,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "取消分享会话",
|
"command.session.unshare": "取消分享会话",
|
||||||
"command.session.unshare.description": "停止分享此会话",
|
"command.session.unshare.description": "停止分享此会话",
|
||||||
|
|
||||||
"palette.search.placeholder": "搜索文件和命令",
|
"palette.search.placeholder": "搜索文件、命令和会话",
|
||||||
"palette.empty": "未找到结果",
|
"palette.empty": "未找到结果",
|
||||||
"palette.group.commands": "命令",
|
"palette.group.commands": "命令",
|
||||||
"palette.group.files": "文件",
|
"palette.group.files": "文件",
|
||||||
@@ -344,7 +344,12 @@ export const dict = {
|
|||||||
"toast.language.description": "已切换到{{language}}",
|
"toast.language.description": "已切换到{{language}}",
|
||||||
|
|
||||||
"toast.theme.title": "主题已切换",
|
"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.title": "自动接受编辑",
|
||||||
"toast.permissions.autoaccept.on.description": "编辑和写入权限将自动获批",
|
"toast.permissions.autoaccept.on.description": "编辑和写入权限将自动获批",
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export const dict = {
|
|||||||
|
|
||||||
"command.session.new": "新增工作階段",
|
"command.session.new": "新增工作階段",
|
||||||
"command.file.open": "開啟檔案",
|
"command.file.open": "開啟檔案",
|
||||||
"command.file.open.description": "搜尋檔案和命令",
|
|
||||||
"command.context.addSelection": "將選取內容加入上下文",
|
"command.context.addSelection": "將選取內容加入上下文",
|
||||||
"command.context.addSelection.description": "加入目前檔案中選取的行",
|
"command.context.addSelection.description": "加入目前檔案中選取的行",
|
||||||
"command.terminal.toggle": "切換終端機",
|
"command.terminal.toggle": "切換終端機",
|
||||||
@@ -74,6 +73,7 @@ export const dict = {
|
|||||||
"command.model.variant.cycle.description": "切換到下一個強度等級",
|
"command.model.variant.cycle.description": "切換到下一個強度等級",
|
||||||
"command.permissions.autoaccept.enable": "自動接受編輯",
|
"command.permissions.autoaccept.enable": "自動接受編輯",
|
||||||
"command.permissions.autoaccept.disable": "停止自動接受編輯",
|
"command.permissions.autoaccept.disable": "停止自動接受編輯",
|
||||||
|
"command.workspace.toggle": "切換工作區",
|
||||||
"command.session.undo": "復原",
|
"command.session.undo": "復原",
|
||||||
"command.session.undo.description": "復原上一則訊息",
|
"command.session.undo.description": "復原上一則訊息",
|
||||||
"command.session.redo": "重做",
|
"command.session.redo": "重做",
|
||||||
@@ -87,7 +87,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "取消分享工作階段",
|
"command.session.unshare": "取消分享工作階段",
|
||||||
"command.session.unshare.description": "停止分享此工作階段",
|
"command.session.unshare.description": "停止分享此工作階段",
|
||||||
|
|
||||||
"palette.search.placeholder": "搜尋檔案和命令",
|
"palette.search.placeholder": "搜尋檔案、命令和工作階段",
|
||||||
"palette.empty": "找不到結果",
|
"palette.empty": "找不到結果",
|
||||||
"palette.group.commands": "命令",
|
"palette.group.commands": "命令",
|
||||||
"palette.group.files": "檔案",
|
"palette.group.files": "檔案",
|
||||||
@@ -341,7 +341,12 @@ export const dict = {
|
|||||||
"toast.language.description": "已切換到 {{language}}",
|
"toast.language.description": "已切換到 {{language}}",
|
||||||
|
|
||||||
"toast.theme.title": "主題已切換",
|
"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.title": "自動接受編輯",
|
||||||
"toast.permissions.autoaccept.on.description": "編輯和寫入權限將自動獲准",
|
"toast.permissions.autoaccept.on.description": "編輯和寫入權限將自動獲准",
|
||||||
|
|||||||
@@ -269,14 +269,14 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
|||||||
<div class="flex flex-col items-center gap-2">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<div class="flex items-center justify-center gap-1">
|
<div class="flex items-center justify-center gap-1">
|
||||||
{language.t("error.page.report.prefix")}
|
{language.t("error.page.report.prefix")}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center text-text-interactive-base gap-1"
|
class="flex items-center text-text-interactive-base gap-1"
|
||||||
onClick={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
onClick={() => void platform.openLink("https://opencode.ai/desktop-feedback").catch(() => undefined)}
|
||||||
>
|
>
|
||||||
<div>{language.t("error.page.report.discord")}</div>
|
<div>{language.t("error.page.report.discord")}</div>
|
||||||
<Icon name="discord" class="text-text-interactive-base" />
|
<Icon name="discord" class="text-text-interactive-base" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Show when={platform.version}>
|
<Show when={platform.version}>
|
||||||
{(version) => (
|
{(version) => (
|
||||||
|
|||||||
+270
-151
@@ -31,6 +31,7 @@ import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
|||||||
import { HoverCard } from "@opencode-ai/ui/hover-card"
|
import { HoverCard } from "@opencode-ai/ui/hover-card"
|
||||||
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
|
import { ContextMenu } from "@opencode-ai/ui/context-menu"
|
||||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
@@ -108,7 +109,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const initialDir = params.dir
|
const initialDirectory = decode64(params.dir)
|
||||||
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
|
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
|
||||||
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
|
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
|
||||||
const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = {
|
const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = {
|
||||||
@@ -119,7 +120,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
|
const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
|
||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
autoselect: !params.dir,
|
autoselect: !initialDirectory,
|
||||||
busyWorkspaces: new Set<string>(),
|
busyWorkspaces: new Set<string>(),
|
||||||
hoverSession: undefined as string | undefined,
|
hoverSession: undefined as string | undefined,
|
||||||
hoverProject: undefined as string | undefined,
|
hoverProject: undefined as string | undefined,
|
||||||
@@ -179,13 +180,21 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const autoselecting = createMemo(() => {
|
const autoselecting = createMemo(() => {
|
||||||
if (params.dir) return false
|
if (params.dir) return false
|
||||||
if (initialDir) return false
|
|
||||||
if (!state.autoselect) return false
|
if (!state.autoselect) return false
|
||||||
if (!pageReady()) return true
|
if (!pageReady()) return true
|
||||||
if (!layoutReady()) return true
|
if (!layoutReady()) return true
|
||||||
const list = layout.projects.list()
|
const list = layout.projects.list()
|
||||||
if (list.length === 0) return false
|
if (list.length > 0) return true
|
||||||
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
|
const editorOpen = (id: string) => editor.active === id
|
||||||
@@ -498,7 +507,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const bUpdated = b.time.updated ?? b.time.created
|
const bUpdated = b.time.updated ?? b.time.created
|
||||||
const aRecent = aUpdated > oneMinuteAgo
|
const aRecent = aUpdated > oneMinuteAgo
|
||||||
const bRecent = bUpdated > 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
|
||||||
if (!aRecent && bRecent) return 1
|
if (!aRecent && bRecent) return 1
|
||||||
return bUpdated - aUpdated
|
return bUpdated - aUpdated
|
||||||
@@ -565,11 +574,18 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!value.ready) return
|
if (!value.ready) return
|
||||||
if (!value.layoutReady) return
|
if (!value.layoutReady) return
|
||||||
if (!state.autoselect) return
|
if (!state.autoselect) return
|
||||||
if (initialDir) return
|
|
||||||
if (value.dir) return
|
if (value.dir) return
|
||||||
if (value.list.length === 0) return
|
|
||||||
|
|
||||||
const last = server.projects.last()
|
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]
|
const next = value.list.find((project) => project.worktree === last) ?? value.list[0]
|
||||||
if (!next) return
|
if (!next) return
|
||||||
setState("autoselect", false)
|
setState("autoselect", false)
|
||||||
@@ -738,7 +754,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function prefetchMessages(directory: string, sessionID: string, token: number) {
|
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 }))
|
return retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
|
||||||
.then((messages) => {
|
.then((messages) => {
|
||||||
@@ -749,23 +765,49 @@ export default function Layout(props: ParentProps) {
|
|||||||
.map((x) => x.info)
|
.map((x) => x.info)
|
||||||
.filter((m) => !!m?.id)
|
.filter((m) => !!m?.id)
|
||||||
.slice()
|
.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(() => {
|
batch(() => {
|
||||||
setStore("message", sessionID, reconcile(next, { key: "id" }))
|
setStore("message", sessionID, reconcile(merged, { key: "id" }))
|
||||||
|
|
||||||
for (const message of items) {
|
for (const message of items) {
|
||||||
setStore(
|
const currentParts = store.part[message.info.id] ?? []
|
||||||
"part",
|
const mergedParts = (() => {
|
||||||
message.info.id,
|
if (currentParts.length === 0) {
|
||||||
reconcile(
|
return message.parts
|
||||||
message.parts
|
|
||||||
.filter((p) => !!p?.id)
|
.filter((p) => !!p?.id)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||||
{ key: "id" },
|
}
|
||||||
),
|
|
||||||
)
|
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" }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1095,6 +1137,29 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (session) archiveSession(session)
|
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",
|
id: "theme.cycle",
|
||||||
title: language.t("command.theme.cycle"),
|
title: language.t("command.theme.cycle"),
|
||||||
@@ -2310,10 +2375,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
() => props.project.vcs === "git" && layout.sidebar.workspaces(props.project.worktree)(),
|
() => props.project.vcs === "git" && layout.sidebar.workspaces(props.project.worktree)(),
|
||||||
)
|
)
|
||||||
const [open, setOpen] = createSignal(false)
|
const [open, setOpen] = createSignal(false)
|
||||||
|
const [menu, setMenu] = createSignal(false)
|
||||||
|
|
||||||
const preview = createMemo(() => !props.mobile && layout.sidebar.opened())
|
const preview = createMemo(() => !props.mobile && layout.sidebar.opened())
|
||||||
const overlay = 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(() => {
|
createEffect(() => {
|
||||||
if (preview()) return
|
if (preview()) return
|
||||||
@@ -2351,50 +2419,95 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const projectName = () => props.project.name || getFilename(props.project.worktree)
|
const projectName = () => props.project.name || getFilename(props.project.worktree)
|
||||||
const trigger = (
|
const Trigger = () => (
|
||||||
<button
|
<ContextMenu
|
||||||
type="button"
|
modal={!sidebarHovering()}
|
||||||
aria-label={projectName()}
|
onOpenChange={(value) => {
|
||||||
data-action="project-switch"
|
setMenu(value)
|
||||||
data-project={base64Encode(props.project.worktree)}
|
if (value) setOpen(false)
|
||||||
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={() => {
|
|
||||||
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 />
|
<ContextMenu.Trigger
|
||||||
</button>
|
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={() => {
|
||||||
|
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 />
|
||||||
|
</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 (
|
return (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||||
<Show when={preview()} fallback={trigger}>
|
<Show when={preview()} fallback={<Trigger />}>
|
||||||
<HoverCard
|
<HoverCard
|
||||||
open={open()}
|
open={open() && !menu()}
|
||||||
openDelay={0}
|
openDelay={0}
|
||||||
closeDelay={0}
|
closeDelay={0}
|
||||||
placement="right-start"
|
placement="right-start"
|
||||||
gutter={6}
|
gutter={6}
|
||||||
trigger={trigger}
|
trigger={<Trigger />}
|
||||||
onOpenChange={(value) => {
|
onOpenChange={(value) => {
|
||||||
|
if (menu()) return
|
||||||
setOpen(value)
|
setOpen(value)
|
||||||
if (value) setState("hoverSession", undefined)
|
if (value) setState("hoverSession", undefined)
|
||||||
}}
|
}}
|
||||||
@@ -2615,7 +2728,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}}
|
}}
|
||||||
style={{ width: panelProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }}
|
style={{ width: panelProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }}
|
||||||
>
|
>
|
||||||
<Show when={panelProps.project} keyed>
|
<Show when={panelProps.project}>
|
||||||
{(p) => (
|
{(p) => (
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 px-2 py-1">
|
<div class="shrink-0 px-2 py-1">
|
||||||
@@ -2624,7 +2737,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
<InlineEditor
|
<InlineEditor
|
||||||
id={`project:${projectId()}`}
|
id={`project:${projectId()}`}
|
||||||
value={projectName}
|
value={projectName}
|
||||||
onSave={(next) => renameProject(p, next)}
|
onSave={(next) => renameProject(p(), next)}
|
||||||
class="text-16-medium text-text-strong truncate"
|
class="text-16-medium text-text-strong truncate"
|
||||||
displayClass="text-16-medium text-text-strong truncate"
|
displayClass="text-16-medium text-text-strong truncate"
|
||||||
stopPropagation
|
stopPropagation
|
||||||
@@ -2633,7 +2746,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
gutter={2}
|
gutter={2}
|
||||||
value={p.worktree}
|
value={p().worktree}
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
"max-width": "640px",
|
"max-width": "640px",
|
||||||
@@ -2641,7 +2754,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="text-12-regular text-text-base truncate select-text">
|
<span class="text-12-regular text-text-base truncate select-text">
|
||||||
{p.worktree.replace(homedir(), "~")}
|
{p().worktree.replace(homedir(), "~")}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -2652,31 +2765,31 @@ export default function Layout(props: ParentProps) {
|
|||||||
icon="dot-grid"
|
icon="dot-grid"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
data-action="project-menu"
|
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"
|
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")}
|
aria-label={language.t("common.moreOptions")}
|
||||||
/>
|
/>
|
||||||
<DropdownMenu.Portal mount={!panelProps.mobile ? state.nav : undefined}>
|
<DropdownMenu.Portal mount={!panelProps.mobile ? state.nav : undefined}>
|
||||||
<DropdownMenu.Content class="mt-1">
|
<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.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
data-action="project-workspaces-toggle"
|
data-action="project-workspaces-toggle"
|
||||||
data-project={base64Encode(p.worktree)}
|
data-project={base64Encode(p().worktree)}
|
||||||
disabled={p.vcs !== "git" && !layout.sidebar.workspaces(p.worktree)()}
|
disabled={p().vcs !== "git" && !layout.sidebar.workspaces(p().worktree)()}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
const enabled = layout.sidebar.workspaces(p.worktree)()
|
const enabled = layout.sidebar.workspaces(p().worktree)()
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
layout.sidebar.toggleWorkspaces(p.worktree)
|
layout.sidebar.toggleWorkspaces(p().worktree)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (p.vcs !== "git") return
|
if (p().vcs !== "git") return
|
||||||
layout.sidebar.toggleWorkspaces(p.worktree)
|
layout.sidebar.toggleWorkspaces(p().worktree)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>
|
||||||
{layout.sidebar.workspaces(p.worktree)()
|
{layout.sidebar.workspaces(p().worktree)()
|
||||||
? language.t("sidebar.workspaces.disable")
|
? language.t("sidebar.workspaces.disable")
|
||||||
: language.t("sidebar.workspaces.enable")}
|
: language.t("sidebar.workspaces.enable")}
|
||||||
</DropdownMenu.ItemLabel>
|
</DropdownMenu.ItemLabel>
|
||||||
@@ -2684,8 +2797,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
data-action="project-close-menu"
|
data-action="project-close-menu"
|
||||||
data-project={base64Encode(p.worktree)}
|
data-project={base64Encode(p().worktree)}
|
||||||
onSelect={() => closeProject(p.worktree)}
|
onSelect={() => closeProject(p().worktree)}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
@@ -2695,103 +2808,109 @@ export default function Layout(props: ParentProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show
|
<div class="flex-1 min-h-0 flex flex-col">
|
||||||
when={workspacesEnabled()}
|
<Show
|
||||||
fallback={
|
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
|
<TooltipKeybind
|
||||||
title={language.t("command.session.new")}
|
title={language.t("workspace.new")}
|
||||||
keybind={command.keybind("session.new")}
|
keybind={command.keybind("workspace.new")}
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<Button
|
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}>
|
||||||
size="large"
|
{language.t("workspace.new")}
|
||||||
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>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-h-0">
|
<div class="relative flex-1 min-h-0">
|
||||||
<LocalWorkspace project={p} mobile={panelProps.mobile} />
|
<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>
|
||||||
</>
|
</>
|
||||||
}
|
</Show>
|
||||||
>
|
</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>
|
</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
|
||||||
<div class="rounded-md bg-background-base shadow-xs-border-base">
|
class="shrink-0 px-2 py-3 border-t border-border-weak-base"
|
||||||
<div class="p-3 flex flex-col gap-2">
|
classList={{
|
||||||
<div class="text-12-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div>
|
hidden: !(providers.all().length > 0 && providers.paid().length === 0),
|
||||||
<div class="text-text-base">{language.t("sidebar.gettingStarted.line1")}</div>
|
}}
|
||||||
<div class="text-text-base">{language.t("sidebar.gettingStarted.line2")}</div>
|
>
|
||||||
</div>
|
<div class="rounded-md bg-background-base shadow-xs-border-base">
|
||||||
<Button
|
<div class="p-3 flex flex-col gap-2">
|
||||||
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"
|
<div class="text-12-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div>
|
||||||
size="large"
|
<div class="text-text-base">{language.t("sidebar.gettingStarted.line1")}</div>
|
||||||
icon="plus"
|
<div class="text-text-base">{language.t("sidebar.gettingStarted.line2")}</div>
|
||||||
onClick={connectProvider}
|
|
||||||
>
|
|
||||||
{language.t("command.provider.connect")}
|
|
||||||
</Button>
|
|
||||||
</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>
|
</div>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2876,7 +2995,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
icon="help"
|
icon="help"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
onClick={() => void platform.openLink("https://opencode.ai/desktop-feedback").catch(() => undefined)}
|
||||||
aria-label={language.t("sidebar.help")}
|
aria-label={language.t("sidebar.help")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -500,9 +500,7 @@ export default function Page() {
|
|||||||
const out = new Map<string, "add" | "del" | "mix">()
|
const out = new Map<string, "add" | "del" | "mix">()
|
||||||
for (const diff of diffs()) {
|
for (const diff of diffs()) {
|
||||||
const file = normalize(diff.file)
|
const file = normalize(diff.file)
|
||||||
const add = diff.additions > 0
|
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
|
||||||
const del = diff.deletions > 0
|
|
||||||
const kind = add && del ? "mix" : add ? "add" : del ? "del" : "mix"
|
|
||||||
|
|
||||||
out.set(file, kind)
|
out.set(file, kind)
|
||||||
|
|
||||||
@@ -683,7 +681,7 @@ export default function Page() {
|
|||||||
{
|
{
|
||||||
id: "file.open",
|
id: "file.open",
|
||||||
title: language.t("command.file.open"),
|
title: language.t("command.file.open"),
|
||||||
description: language.t("command.file.open.description"),
|
description: language.t("palette.search.placeholder"),
|
||||||
category: language.t("command.category.file"),
|
category: language.t("command.category.file"),
|
||||||
keybind: "mod+p",
|
keybind: "mod+p",
|
||||||
slash: "open",
|
slash: "open",
|
||||||
@@ -1773,7 +1771,7 @@ export default function Page() {
|
|||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true,
|
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true,
|
||||||
"flex-1 pt-6 md:pt-3": true,
|
"flex-1 pt-2 md:pt-3": true,
|
||||||
"md:flex-none": layout.fileTree.opened(),
|
"md:flex-none": layout.fileTree.opened(),
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
@@ -1952,7 +1950,7 @@ export default function Page() {
|
|||||||
"sticky top-0 z-30 bg-background-stronger": true,
|
"sticky top-0 z-30 bg-background-stronger": true,
|
||||||
"w-full": true,
|
"w-full": true,
|
||||||
"px-4 md:px-6": true,
|
"px-4 md:px-6": true,
|
||||||
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px] 3xl:mx-auto 4xl:max-w-[1600px] 4xl:mx-auto 5xl:max-w-[1900px] 5xl:mx-auto":
|
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px] 4xl:max-w-[1600px] 5xl:max-w-[1900px]":
|
||||||
centered(),
|
centered(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1978,10 +1976,10 @@ export default function Page() {
|
|||||||
<div
|
<div
|
||||||
ref={autoScroll.contentRef}
|
ref={autoScroll.contentRef}
|
||||||
role="log"
|
role="log"
|
||||||
class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
class="flex flex-col gap-12 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
||||||
classList={{
|
classList={{
|
||||||
"w-full": true,
|
"w-full": true,
|
||||||
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px] 3xl:mx-auto 4xl:max-w-[1600px] 4xl:mx-auto 5xl:max-w-[1900px] 5xl:mx-auto":
|
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px] 4xl:max-w-[1600px] 5xl:max-w-[1900px]":
|
||||||
centered(),
|
centered(),
|
||||||
"mt-0.5": centered(),
|
"mt-0.5": centered(),
|
||||||
"mt-0": !centered(),
|
"mt-0": !centered(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export async function handler(
|
|||||||
const dataDumper = createDataDumper(sessionId, requestId, projectId)
|
const dataDumper = createDataDumper(sessionId, requestId, projectId)
|
||||||
const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
|
const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
|
||||||
const isTrial = await trialLimiter?.isTrial()
|
const isTrial = await trialLimiter?.isTrial()
|
||||||
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip)
|
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request.headers)
|
||||||
await rateLimiter?.check()
|
await rateLimiter?.check()
|
||||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
||||||
const stickyProvider = await stickyTracker?.get()
|
const stickyProvider = await stickyTracker?.get()
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { RateLimitError } from "./error"
|
|||||||
import { logger } from "./logger"
|
import { logger } from "./logger"
|
||||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
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
|
if (!limit) return
|
||||||
|
|
||||||
|
const limitValue = limit.checkHeader && !headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value
|
||||||
|
|
||||||
const ip = !rawIp.length ? "unknown" : rawIp
|
const ip = !rawIp.length ? "unknown" : rawIp
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const intervals =
|
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)
|
const total = rows.reduce((sum, r) => sum + r.count, 0)
|
||||||
logger.debug(`rate limit total: ${total}`)
|
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",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export namespace ZenData {
|
|||||||
const RateLimitSchema = z.object({
|
const RateLimitSchema = z.object({
|
||||||
period: z.enum(["day", "rolling"]),
|
period: z.enum(["day", "rolling"]),
|
||||||
value: z.number().int(),
|
value: z.number().int(),
|
||||||
|
checkHeader: z.string().optional(),
|
||||||
|
fallbackValue: z.number().int().optional(),
|
||||||
})
|
})
|
||||||
export type Format = z.infer<typeof FormatSchema>
|
export type Format = z.infer<typeof FormatSchema>
|
||||||
export type Trial = z.infer<typeof TrialSchema>
|
export type Trial = z.infer<typeof TrialSchema>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<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" />
|
<div data-tauri-decorum-tb class="w-0 h-0 hidden" />
|
||||||
<script src="/src/index.tsx" type="module"></script>
|
<script src="/src/index.tsx" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
|
{
|
||||||
|
"identifier": "opener:allow-open-path",
|
||||||
|
"allow": [{ "path": "/**", "app": true }]
|
||||||
|
},
|
||||||
"deep-link:default",
|
"deep-link:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-theme",
|
"core:window:allow-set-theme",
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
// This file has been generated by Tauri Specta. Do not edit this file manually.
|
// This file has been generated by Tauri Specta. Do not edit this file manually.
|
||||||
|
|
||||||
import { invoke as __TAURI_INVOKE, Channel } from "@tauri-apps/api/core"
|
import { invoke as __TAURI_INVOKE, Channel } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
/** Commands */
|
/** Commands */
|
||||||
export const commands = {
|
export const commands = {
|
||||||
killSidecar: () => __TAURI_INVOKE<void>("kill_sidecar"),
|
killSidecar: () => __TAURI_INVOKE<void>("kill_sidecar"),
|
||||||
installCli: () => __TAURI_INVOKE<string>("install_cli"),
|
installCli: () => __TAURI_INVOKE<string>("install_cli"),
|
||||||
ensureServerReady: () => __TAURI_INVOKE<ServerReadyData>("ensure_server_ready"),
|
ensureServerReady: () => __TAURI_INVOKE<ServerReadyData>("ensure_server_ready"),
|
||||||
getDefaultServerUrl: () => __TAURI_INVOKE<string | null>("get_default_server_url"),
|
getDefaultServerUrl: () => __TAURI_INVOKE<string | null>("get_default_server_url"),
|
||||||
setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE<null>("set_default_server_url", { url }),
|
setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE<null>("set_default_server_url", { url }),
|
||||||
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
|
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
|
||||||
}
|
};
|
||||||
|
|
||||||
/* Types */
|
/* Types */
|
||||||
export type ServerReadyData = {
|
export type ServerReadyData = {
|
||||||
url: string
|
url: string,
|
||||||
password: string | null
|
password: string | null,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { render } from "solid-js/web"
|
|||||||
import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app"
|
import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app"
|
||||||
import { open, save } from "@tauri-apps/plugin-dialog"
|
import { open, save } from "@tauri-apps/plugin-dialog"
|
||||||
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
|
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
|
||||||
import { open as shellOpen } from "@tauri-apps/plugin-shell"
|
import { openPath, openUrl } from "@tauri-apps/plugin-opener"
|
||||||
import { type as ostype } from "@tauri-apps/plugin-os"
|
import { type as ostype } from "@tauri-apps/plugin-os"
|
||||||
import { check, Update } from "@tauri-apps/plugin-updater"
|
import { check, Update } from "@tauri-apps/plugin-updater"
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window"
|
import { getCurrentWindow } from "@tauri-apps/api/window"
|
||||||
@@ -94,8 +94,10 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
|
|||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
|
|
||||||
openLink(url: string) {
|
openLink(url: string, openWith?: string) {
|
||||||
void shellOpen(url).catch(() => undefined)
|
const isUrl = /^(https?:|mailto:|tel:|opencode:)/.test(url)
|
||||||
|
if (isUrl) return openUrl(url, openWith)
|
||||||
|
return openPath(url, openWith)
|
||||||
},
|
},
|
||||||
|
|
||||||
back() {
|
back() {
|
||||||
@@ -359,7 +361,7 @@ render(() => {
|
|||||||
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
|
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
|
||||||
if (link?.href) {
|
if (link?.href) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
platform.openLink(link.href)
|
void platform.openLink(link.href).catch(() => undefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.1.48"
|
version = "1.1.51"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/anomalyco/opencode"
|
repository = "https://github.com/anomalyco/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[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.51/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[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.51/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[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.51/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[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.51/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[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.51/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.1.48",
|
"version": "1.1.51",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode"
|
"opencode": "./bin/opencode"
|
||||||
},
|
},
|
||||||
|
"randomField": "this-is-a-random-value-12345",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": "./src/*.ts"
|
"./*": "./src/*.ts"
|
||||||
},
|
},
|
||||||
@@ -70,7 +71,7 @@
|
|||||||
"@ai-sdk/vercel": "1.0.33",
|
"@ai-sdk/vercel": "1.0.33",
|
||||||
"@ai-sdk/xai": "2.0.56",
|
"@ai-sdk/xai": "2.0.56",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@gitlab/gitlab-ai-provider": "3.3.1",
|
"@gitlab/gitlab-ai-provider": "3.4.0",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.25.2",
|
"@modelcontextprotocol/sdk": "1.25.2",
|
||||||
@@ -91,8 +92,9 @@
|
|||||||
"@standard-schema/spec": "1.0.0",
|
"@standard-schema/spec": "1.0.0",
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
|
"ai-gateway-provider": "2.3.1",
|
||||||
"bonjour-service": "1.3.0",
|
"bonjour-service": "1.3.0",
|
||||||
"bun-pty": "0.4.4",
|
"bun-pty": "0.4.8",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "4.0.3",
|
||||||
"clipboardy": "4.0.0",
|
"clipboardy": "4.0.0",
|
||||||
"decimal.js": "10.5.0",
|
"decimal.js": "10.5.0",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const version = Object.values(binaries)[0]
|
|||||||
await $`mkdir -p ./dist/${pkg.name}`
|
await $`mkdir -p ./dist/${pkg.name}`
|
||||||
await $`cp -r ./bin ./dist/${pkg.name}/bin`
|
await $`cp -r ./bin ./dist/${pkg.name}/bin`
|
||||||
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
|
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(
|
await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
@@ -30,6 +31,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
|||||||
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
|
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
|
||||||
},
|
},
|
||||||
version: version,
|
version: version,
|
||||||
|
license: pkg.license,
|
||||||
optionalDependencies: binaries,
|
optionalDependencies: binaries,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { mergeDeep, pipe, sortBy, values } from "remeda"
|
|||||||
import { Global } from "@/global"
|
import { Global } from "@/global"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Plugin } from "@/plugin"
|
import { Plugin } from "@/plugin"
|
||||||
|
import { Skill } from "../skill"
|
||||||
|
|
||||||
export namespace Agent {
|
export namespace Agent {
|
||||||
export const Info = z
|
export const Info = z
|
||||||
@@ -50,13 +51,14 @@ export namespace Agent {
|
|||||||
const state = Instance.state(async () => {
|
const state = Instance.state(async () => {
|
||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
|
|
||||||
|
const skillDirs = await Skill.dirs()
|
||||||
const defaults = PermissionNext.fromConfig({
|
const defaults = PermissionNext.fromConfig({
|
||||||
"*": "allow",
|
"*": "allow",
|
||||||
doom_loop: "ask",
|
doom_loop: "ask",
|
||||||
external_directory: {
|
external_directory: {
|
||||||
"*": "ask",
|
"*": "ask",
|
||||||
[Truncate.DIR]: "allow",
|
|
||||||
[Truncate.GLOB]: "allow",
|
[Truncate.GLOB]: "allow",
|
||||||
|
...Object.fromEntries(skillDirs.map((dir) => [path.join(dir, "*"), "allow"])),
|
||||||
},
|
},
|
||||||
question: "deny",
|
question: "deny",
|
||||||
plan_enter: "deny",
|
plan_enter: "deny",
|
||||||
@@ -140,7 +142,6 @@ export namespace Agent {
|
|||||||
codesearch: "allow",
|
codesearch: "allow",
|
||||||
read: "allow",
|
read: "allow",
|
||||||
external_directory: {
|
external_directory: {
|
||||||
[Truncate.DIR]: "allow",
|
|
||||||
[Truncate.GLOB]: "allow",
|
[Truncate.GLOB]: "allow",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -229,19 +230,19 @@ export namespace Agent {
|
|||||||
item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}))
|
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) {
|
for (const name in result) {
|
||||||
const agent = result[name]
|
const agent = result[name]
|
||||||
const explicit = agent.permission.some((r) => {
|
const explicit = agent.permission.some((r) => {
|
||||||
if (r.permission !== "external_directory") return false
|
if (r.permission !== "external_directory") return false
|
||||||
if (r.action !== "deny") 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
|
if (explicit) continue
|
||||||
|
|
||||||
result[name].permission = PermissionNext.merge(
|
result[name].permission = PermissionNext.merge(
|
||||||
result[name].permission,
|
result[name].permission,
|
||||||
PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow" } }),
|
PermissionNext.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import path from "path"
|
|||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
import { readableStreamToText } from "bun"
|
import { readableStreamToText } from "bun"
|
||||||
import { createRequire } from "module"
|
|
||||||
import { Lock } from "../util/lock"
|
import { Lock } from "../util/lock"
|
||||||
|
import { PackageRegistry } from "./registry"
|
||||||
|
|
||||||
export namespace BunProc {
|
export namespace BunProc {
|
||||||
const log = Log.create({ service: "bun" })
|
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>) {
|
export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
|
||||||
log.info("running", {
|
log.info("running", {
|
||||||
@@ -75,7 +74,17 @@ export namespace BunProc {
|
|||||||
const dependencies = parsed.dependencies ?? {}
|
const dependencies = parsed.dependencies ?? {}
|
||||||
if (!parsed.dependencies) parsed.dependencies = dependencies
|
if (!parsed.dependencies) parsed.dependencies = dependencies
|
||||||
const modExists = await Filesystem.exists(mod)
|
const modExists = await Filesystem.exists(mod)
|
||||||
if (dependencies[pkg] === version && modExists) return mod
|
const cachedVersion = dependencies[pkg]
|
||||||
|
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
|
||||||
const proxied = !!(
|
const proxied = !!(
|
||||||
process.env.HTTP_PROXY ||
|
process.env.HTTP_PROXY ||
|
||||||
|
|||||||
@@ -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 { cmd } from "./cmd"
|
||||||
import { Flag } from "../../flag/flag"
|
import { Flag } from "../../flag/flag"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
import { Command } from "../../command"
|
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { select } from "@clack/prompts"
|
import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
|
||||||
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"
|
|
||||||
import { Server } from "../../server/server"
|
import { Server } from "../../server/server"
|
||||||
import { Provider } from "../../provider/provider"
|
import { Provider } from "../../provider/provider"
|
||||||
import { Agent } from "../../agent/agent"
|
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]> = {
|
type ToolProps<T extends Tool.Info> = {
|
||||||
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
input: Tool.InferParameters<T>
|
||||||
todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
metadata: Tool.InferMetadata<T>
|
||||||
bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
|
part: ToolPart
|
||||||
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
|
}
|
||||||
glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
|
|
||||||
grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
|
function props<T extends Tool.Info>(part: ToolPart): ToolProps<T> {
|
||||||
list: ["List", UI.Style.TEXT_INFO_BOLD],
|
const state = part.state
|
||||||
read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
|
return {
|
||||||
write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
|
input: state.input as Tool.InferParameters<T>,
|
||||||
websearch: ["Search", UI.Style.TEXT_DIM_BOLD],
|
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({
|
export const RunCommand = cmd({
|
||||||
@@ -91,17 +277,22 @@ export const RunCommand = cmd({
|
|||||||
type: "string",
|
type: "string",
|
||||||
describe: "model variant (provider-specific reasoning effort, e.g., high, max, minimal)",
|
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) => {
|
handler: async (args) => {
|
||||||
let message = [...args.message, ...(args["--"] || [])]
|
let message = [...args.message, ...(args["--"] || [])]
|
||||||
.map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
|
.map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
|
||||||
.join(" ")
|
.join(" ")
|
||||||
|
|
||||||
const fileParts: any[] = []
|
const files: { type: "file"; url: string; filename: string; mime: string }[] = []
|
||||||
if (args.file) {
|
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 resolvedPath = path.resolve(process.cwd(), filePath)
|
||||||
const file = Bun.file(resolvedPath)
|
const file = Bun.file(resolvedPath)
|
||||||
const stats = await file.stat().catch(() => {})
|
const stats = await file.stat().catch(() => {})
|
||||||
@@ -117,7 +308,7 @@ export const RunCommand = cmd({
|
|||||||
const stat = await file.stat()
|
const stat = await file.stat()
|
||||||
const mime = stat.isDirectory() ? "application/x-directory" : "text/plain"
|
const mime = stat.isDirectory() ? "application/x-directory" : "text/plain"
|
||||||
|
|
||||||
fileParts.push({
|
files.push({
|
||||||
type: "file",
|
type: "file",
|
||||||
url: `file://${resolvedPath}`,
|
url: `file://${resolvedPath}`,
|
||||||
filename: path.basename(resolvedPath),
|
filename: path.basename(resolvedPath),
|
||||||
@@ -133,17 +324,75 @@ export const RunCommand = cmd({
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const execute = async (sdk: OpencodeClient, sessionID: string) => {
|
const rules: PermissionNext.Ruleset = [
|
||||||
const printEvent = (color: string, type: string, title: string) => {
|
{
|
||||||
UI.println(
|
permission: "question",
|
||||||
color + `|`,
|
action: "deny",
|
||||||
UI.Style.TEXT_NORMAL + UI.Style.TEXT_DIM + ` ${type.padEnd(7, " ")}`,
|
pattern: "*",
|
||||||
"",
|
},
|
||||||
UI.Style.TEXT_NORMAL + title,
|
{
|
||||||
)
|
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") {
|
if (args.format === "json") {
|
||||||
process.stdout.write(JSON.stringify({ type, timestamp: Date.now(), sessionID, ...data }) + EOL)
|
process.stdout.write(JSON.stringify({ type, timestamp: Date.now(), sessionID, ...data }) + EOL)
|
||||||
return true
|
return true
|
||||||
@@ -152,41 +401,77 @@ export const RunCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const events = await sdk.event.subscribe()
|
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) {
|
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") {
|
if (event.type === "message.part.updated") {
|
||||||
const part = event.properties.part
|
const part = event.properties.part
|
||||||
if (part.sessionID !== sessionID) continue
|
if (part.sessionID !== sessionID) continue
|
||||||
|
|
||||||
if (part.type === "tool" && part.state.status === "completed") {
|
if (part.type === "tool" && part.state.status === "completed") {
|
||||||
if (outputJsonEvent("tool_use", { part })) continue
|
if (emit("tool_use", { part })) continue
|
||||||
const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
|
tool(part)
|
||||||
const title =
|
}
|
||||||
part.state.title ||
|
|
||||||
(Object.keys(part.state.input).length > 0 ? JSON.stringify(part.state.input) : "Unknown")
|
if (
|
||||||
printEvent(color, tool, title)
|
part.type === "tool" &&
|
||||||
if (part.tool === "bash" && part.state.output?.trim()) {
|
part.tool === "task" &&
|
||||||
UI.println()
|
part.state.status === "running" &&
|
||||||
UI.println(part.state.output)
|
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 (part.type === "step-start") {
|
||||||
if (outputJsonEvent("step_start", { part })) continue
|
if (emit("step_start", { part })) continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.type === "step-finish") {
|
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 (part.type === "text" && part.time?.end) {
|
||||||
if (outputJsonEvent("text", { part })) continue
|
if (emit("text", { part })) continue
|
||||||
const isPiped = !process.stdout.isTTY
|
const text = part.text.trim()
|
||||||
if (!isPiped) UI.println()
|
if (!text) continue
|
||||||
process.stdout.write((isPiped ? part.text : UI.markdown(part.text)) + EOL)
|
if (!process.stdout.isTTY) {
|
||||||
if (!isPiped) UI.println()
|
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) {
|
if ("data" in props.error && props.error.data && "message" in props.error.data) {
|
||||||
err = String(props.error.data.message)
|
err = String(props.error.data.message)
|
||||||
}
|
}
|
||||||
errorMsg = errorMsg ? errorMsg + EOL + err : err
|
error = error ? error + EOL + err : err
|
||||||
if (outputJsonEvent("error", { error: props.error })) continue
|
if (emit("error", { error: props.error })) continue
|
||||||
UI.error(err)
|
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
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "permission.asked") {
|
if (event.type === "permission.asked") {
|
||||||
const permission = event.properties
|
const permission = event.properties
|
||||||
if (permission.sessionID !== sessionID) continue
|
if (permission.sessionID !== sessionID) continue
|
||||||
const result = await select({
|
UI.println(
|
||||||
message: `Permission required: ${permission.permission} (${permission.patterns.join(", ")})`,
|
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||||
options: [
|
UI.Style.TEXT_NORMAL +
|
||||||
{ value: "once", label: "Allow once" },
|
`permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`,
|
||||||
{ value: "always", label: "Always allow: " + permission.always.join(", ") },
|
)
|
||||||
{ value: "reject", label: "Reject" },
|
await sdk.permission.reply({
|
||||||
],
|
requestID: permission.id,
|
||||||
initialValue: "once",
|
reply: "reject",
|
||||||
}).catch(() => "reject")
|
|
||||||
const response = (result.toString().includes("cancel") ? "reject" : result) as "once" | "always" | "reject"
|
|
||||||
await sdk.permission.respond({
|
|
||||||
sessionID,
|
|
||||||
permissionID: permission.id,
|
|
||||||
response,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
}
|
||||||
|
|
||||||
// Validate agent if specified
|
// Validate agent if specified
|
||||||
const resolvedAgent = await (async () => {
|
const agent = await (async () => {
|
||||||
if (!args.agent) return undefined
|
if (!args.agent) return undefined
|
||||||
const agent = await Agent.get(args.agent)
|
const entry = await Agent.get(args.agent)
|
||||||
if (!agent) {
|
if (!entry) {
|
||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||||
UI.Style.TEXT_NORMAL,
|
UI.Style.TEXT_NORMAL,
|
||||||
@@ -240,7 +523,7 @@ export const RunCommand = cmd({
|
|||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
if (agent.mode === "subagent") {
|
if (entry.mode === "subagent") {
|
||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||||
UI.Style.TEXT_NORMAL,
|
UI.Style.TEXT_NORMAL,
|
||||||
@@ -251,91 +534,42 @@ export const RunCommand = cmd({
|
|||||||
return args.agent
|
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) {
|
if (args.command) {
|
||||||
await sdk.session.command({
|
await sdk.session.command({
|
||||||
sessionID,
|
sessionID,
|
||||||
agent: resolvedAgent,
|
agent,
|
||||||
model: args.model,
|
model: args.model,
|
||||||
command: args.command,
|
command: args.command,
|
||||||
arguments: message,
|
arguments: message,
|
||||||
variant: args.variant,
|
variant: args.variant,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const modelParam = args.model ? Provider.parseModel(args.model) : undefined
|
const model = args.model ? Provider.parseModel(args.model) : undefined
|
||||||
await sdk.session.prompt({
|
await sdk.session.prompt({
|
||||||
sessionID,
|
sessionID,
|
||||||
agent: resolvedAgent,
|
agent,
|
||||||
model: modelParam,
|
model,
|
||||||
variant: args.variant,
|
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) {
|
if (args.attach) {
|
||||||
const sdk = createOpencodeClient({ baseUrl: args.attach })
|
const sdk = createOpencodeClient({ baseUrl: args.attach })
|
||||||
|
return await execute(sdk)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
@@ -344,52 +578,7 @@ export const RunCommand = cmd({
|
|||||||
return Server.App().fetch(request)
|
return Server.App().fetch(request)
|
||||||
}) as typeof globalThis.fetch
|
}) as typeof globalThis.fetch
|
||||||
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
|
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
|
||||||
|
await execute(sdk)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -187,7 +187,6 @@ function App() {
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
Clipboard.setRenderer(renderer)
|
|
||||||
renderer.disableStdoutInterception()
|
renderer.disableStdoutInterception()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
|
|||||||
@@ -129,6 +129,16 @@ export function Autocomplete(props: {
|
|||||||
return props.input().getTextRange(store.index + 1, props.input().cursorOffset)
|
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
|
// 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
|
// 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.
|
// that the mouseover event doesn't trigger when filtering.
|
||||||
@@ -208,7 +218,7 @@ export function Autocomplete(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [files] = createResource(
|
const [files] = createResource(
|
||||||
() => filter(),
|
() => search(),
|
||||||
async (query) => {
|
async (query) => {
|
||||||
if (!store.visible || store.visible === "/") return []
|
if (!store.visible || store.visible === "/") return []
|
||||||
|
|
||||||
@@ -378,9 +388,9 @@ export function Autocomplete(props: {
|
|||||||
const mixed: AutocompleteOption[] =
|
const mixed: AutocompleteOption[] =
|
||||||
store.visible === "@" ? [...agentsValue, ...(filesValue || []), ...mcpResources()] : [...commandsValue]
|
store.visible === "@" ? [...agentsValue, ...(filesValue || []), ...mcpResources()] : [...commandsValue]
|
||||||
|
|
||||||
const currentFilter = filter()
|
const searchValue = search()
|
||||||
|
|
||||||
if (!currentFilter) {
|
if (!searchValue) {
|
||||||
return mixed
|
return mixed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,7 +398,7 @@ export function Autocomplete(props: {
|
|||||||
return prev
|
return prev
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = fuzzysort.go(removeLineRange(currentFilter), mixed, {
|
const result = fuzzysort.go(removeLineRange(searchValue), mixed, {
|
||||||
keys: [
|
keys: [
|
||||||
(obj) => removeLineRange((obj.value ?? obj.display).trimEnd()),
|
(obj) => removeLineRange((obj.value ?? obj.display).trimEnd()),
|
||||||
"description",
|
"description",
|
||||||
@@ -398,7 +408,7 @@ export function Autocomplete(props: {
|
|||||||
scoreFn: (objResults) => {
|
scoreFn: (objResults) => {
|
||||||
const displayResult = objResults[0]
|
const displayResult = objResults[0]
|
||||||
let score = objResults.score
|
let score = objResults.score
|
||||||
if (displayResult && displayResult.target.startsWith(store.visible + currentFilter)) {
|
if (displayResult && displayResult.target.startsWith(store.visible + searchValue)) {
|
||||||
score *= 2
|
score *= 2
|
||||||
}
|
}
|
||||||
const frecencyScore = objResults.obj.path ? frecency.getFrecency(objResults.obj.path) : 0
|
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 agent = iife(() => {
|
||||||
const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
|
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<{
|
const [agentStore, setAgentStore] = createStore<{
|
||||||
current: string
|
current: string
|
||||||
}>({
|
}>({
|
||||||
@@ -48,6 +49,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
theme.warning,
|
theme.warning,
|
||||||
theme.primary,
|
theme.primary,
|
||||||
theme.error,
|
theme.error,
|
||||||
|
theme.info,
|
||||||
])
|
])
|
||||||
return {
|
return {
|
||||||
list() {
|
list() {
|
||||||
@@ -75,11 +77,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
color(name: string) {
|
color(name: string) {
|
||||||
const all = sync.data.agent
|
const index = visibleAgents().findIndex((x) => x.name === name)
|
||||||
const agent = all.find((x) => x.name === name)
|
|
||||||
if (agent?.color) return RGBA.fromHex(agent.color)
|
|
||||||
const index = all.findIndex((x) => x.name === name)
|
|
||||||
if (index === -1) return colors()[0]
|
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]
|
return colors()[index % colors().length]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import { useRenderer } from "@opentui/solid"
|
|||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { Global } from "@/global"
|
import { Global } from "@/global"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { useSDK } from "./sdk"
|
|
||||||
|
|
||||||
type ThemeColors = {
|
type ThemeColors = {
|
||||||
primary: RGBA
|
primary: RGBA
|
||||||
@@ -429,6 +428,7 @@ export function tint(base: RGBA, overlay: RGBA, alpha: number): RGBA {
|
|||||||
function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
|
function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
|
||||||
const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
|
const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
|
||||||
const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
|
const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
|
||||||
|
const transparent = RGBA.fromInts(0, 0, 0, 0)
|
||||||
const isDark = mode == "dark"
|
const isDark = mode == "dark"
|
||||||
|
|
||||||
const col = (i: number) => {
|
const col = (i: number) => {
|
||||||
@@ -479,8 +479,8 @@ function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJs
|
|||||||
textMuted,
|
textMuted,
|
||||||
selectedListItemText: bg,
|
selectedListItemText: bg,
|
||||||
|
|
||||||
// Background colors
|
// Background colors - use transparent to respect terminal transparency
|
||||||
background: bg,
|
background: transparent,
|
||||||
backgroundPanel: grays[2],
|
backgroundPanel: grays[2],
|
||||||
backgroundElement: grays[3],
|
backgroundElement: grays[3],
|
||||||
backgroundMenu: grays[3],
|
backgroundMenu: grays[3],
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import type { CliRenderer } from "@opentui/core"
|
|
||||||
import { platform, release } from "os"
|
import { platform, release } from "os"
|
||||||
import clipboardy from "clipboardy"
|
import clipboardy from "clipboardy"
|
||||||
import { lazy } from "../../../../util/lazy.js"
|
import { lazy } from "../../../../util/lazy.js"
|
||||||
import { tmpdir } from "os"
|
import { tmpdir } from "os"
|
||||||
import path from "path"
|
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 namespace Clipboard {
|
||||||
export interface Content {
|
export interface Content {
|
||||||
@@ -14,10 +25,6 @@ export namespace Clipboard {
|
|||||||
mime: string
|
mime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setRenderer(renderer: CliRenderer | undefined): void {
|
|
||||||
rendererRef.current = renderer
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function read(): Promise<Content | undefined> {
|
export async function read(): Promise<Content | undefined> {
|
||||||
const os = platform()
|
const os = platform()
|
||||||
|
|
||||||
@@ -146,11 +153,7 @@ export namespace Clipboard {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export async function copy(text: string): Promise<void> {
|
export async function copy(text: string): Promise<void> {
|
||||||
const renderer = rendererRef.current
|
writeOsc52(text)
|
||||||
if (renderer) {
|
|
||||||
const copied = renderer.copyToClipboardOSC52(text)
|
|
||||||
if (copied) return
|
|
||||||
}
|
|
||||||
await getCopyMethod()(text)
|
await getCopyMethod()(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const WebCommand = cmd({
|
|||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_INFO_BOLD + " mDNS: ",
|
UI.Style.TEXT_INFO_BOLD + " mDNS: ",
|
||||||
UI.Style.TEXT_NORMAL,
|
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)",
|
describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)",
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"mdns-domain": {
|
||||||
|
type: "string" as const,
|
||||||
|
describe: "custom domain name for mDNS service (default: opencode.local)",
|
||||||
|
default: "opencode.local",
|
||||||
|
},
|
||||||
cors: {
|
cors: {
|
||||||
type: "string" as const,
|
type: "string" as const,
|
||||||
array: true,
|
array: true,
|
||||||
@@ -36,9 +41,11 @@ export async function resolveNetworkOptions(args: NetworkOptions) {
|
|||||||
const portExplicitlySet = process.argv.includes("--port")
|
const portExplicitlySet = process.argv.includes("--port")
|
||||||
const hostnameExplicitlySet = process.argv.includes("--hostname")
|
const hostnameExplicitlySet = process.argv.includes("--hostname")
|
||||||
const mdnsExplicitlySet = process.argv.includes("--mdns")
|
const mdnsExplicitlySet = process.argv.includes("--mdns")
|
||||||
|
const mdnsDomainExplicitlySet = process.argv.includes("--mdns-domain")
|
||||||
const corsExplicitlySet = process.argv.includes("--cors")
|
const corsExplicitlySet = process.argv.includes("--cors")
|
||||||
|
|
||||||
const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
|
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 port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)
|
||||||
const hostname = hostnameExplicitlySet
|
const hostname = hostnameExplicitlySet
|
||||||
? args.hostname
|
? 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 argsCors = Array.isArray(args.cors) ? args.cors : args.cors ? [args.cors] : []
|
||||||
const cors = [...configCors, ...argsCors]
|
const cors = [...configCors, ...argsCors]
|
||||||
|
|
||||||
return { hostname, port, mdns, cors }
|
return { hostname, port, mdns, mdnsDomain, cors }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { existsSync } from "fs"
|
|||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { Event } from "../server/event"
|
import { Event } from "../server/event"
|
||||||
|
import { PackageRegistry } from "@/bun/registry"
|
||||||
|
|
||||||
export namespace Config {
|
export namespace Config {
|
||||||
const log = Log.create({ service: "config" })
|
const log = Log.create({ service: "config" })
|
||||||
@@ -154,9 +155,10 @@ export namespace Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exists = existsSync(path.join(dir, "node_modules"))
|
const shouldInstall = await needsInstall(dir)
|
||||||
const installing = installDependencies(dir)
|
if (shouldInstall) {
|
||||||
if (!exists) await installing
|
await installDependencies(dir)
|
||||||
|
}
|
||||||
|
|
||||||
result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
|
result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
|
||||||
result.agent = mergeDeep(result.agent, await loadAgent(dir))
|
result.agent = mergeDeep(result.agent, await loadAgent(dir))
|
||||||
@@ -235,6 +237,7 @@ export namespace Config {
|
|||||||
|
|
||||||
export async function installDependencies(dir: string) {
|
export async function installDependencies(dir: string) {
|
||||||
const pkg = path.join(dir, "package.json")
|
const pkg = path.join(dir, "package.json")
|
||||||
|
const targetVersion = Installation.isLocal() ? "latest" : Installation.VERSION
|
||||||
|
|
||||||
if (!(await Bun.file(pkg).exists())) {
|
if (!(await Bun.file(pkg).exists())) {
|
||||||
await Bun.write(pkg, "{}")
|
await Bun.write(pkg, "{}")
|
||||||
@@ -244,18 +247,43 @@ export namespace Config {
|
|||||||
const hasGitIgnore = await Bun.file(gitignore).exists()
|
const hasGitIgnore = await Bun.file(gitignore).exists()
|
||||||
if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
|
if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
|
||||||
|
|
||||||
await BunProc.run(
|
await BunProc.run(["add", `@opencode-ai/plugin@${targetVersion}`, "--exact"], {
|
||||||
["add", "@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION), "--exact"],
|
cwd: dir,
|
||||||
{
|
}).catch(() => {})
|
||||||
cwd: dir,
|
|
||||||
},
|
|
||||||
).catch(() => {})
|
|
||||||
|
|
||||||
// Install any additional dependencies defined in the package.json
|
// Install any additional dependencies defined in the package.json
|
||||||
// This allows local plugins and custom tools to use external packages
|
// This allows local plugins and custom tools to use external packages
|
||||||
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
|
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function needsInstall(dir: string) {
|
||||||
|
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 false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function rel(item: string, patterns: string[]) {
|
function rel(item: string, patterns: string[]) {
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
const index = item.indexOf(pattern)
|
const index = item.indexOf(pattern)
|
||||||
@@ -617,10 +645,12 @@ export namespace Config {
|
|||||||
.describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
|
.describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
|
||||||
options: z.record(z.string(), z.any()).optional(),
|
options: z.record(z.string(), z.any()).optional(),
|
||||||
color: z
|
color: z
|
||||||
.string()
|
.union([
|
||||||
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")
|
z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"),
|
||||||
|
z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||||
|
])
|
||||||
.optional()
|
.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
|
steps: z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
@@ -860,6 +890,7 @@ export namespace Config {
|
|||||||
port: z.number().int().positive().optional().describe("Port to listen on"),
|
port: z.number().int().positive().optional().describe("Port to listen on"),
|
||||||
hostname: z.string().optional().describe("Hostname to listen on"),
|
hostname: z.string().optional().describe("Hostname to listen on"),
|
||||||
mdns: z.boolean().optional().describe("Enable mDNS service discovery"),
|
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"),
|
cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export namespace Flag {
|
|||||||
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT")
|
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT")
|
||||||
export const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS =
|
export const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS =
|
||||||
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
|
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
|
||||||
|
export const OPENCODE_DISABLE_EXTERNAL_SKILLS =
|
||||||
|
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS || truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS")
|
||||||
export declare const OPENCODE_DISABLE_PROJECT_CONFIG: boolean
|
export declare const OPENCODE_DISABLE_PROJECT_CONFIG: boolean
|
||||||
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
|
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
|
||||||
export declare const OPENCODE_CLIENT: string
|
export declare const OPENCODE_CLIENT: string
|
||||||
|
|||||||
@@ -732,7 +732,7 @@ export namespace LSPServer {
|
|||||||
|
|
||||||
export const CSharp: Info = {
|
export const CSharp: Info = {
|
||||||
id: "csharp",
|
id: "csharp",
|
||||||
root: NearestRoot([".sln", ".csproj", "global.json"]),
|
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
|
||||||
extensions: [".cs"],
|
extensions: [".cs"],
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let bin = Bun.which("csharp-ls", {
|
let bin = Bun.which("csharp-ls", {
|
||||||
@@ -772,7 +772,7 @@ export namespace LSPServer {
|
|||||||
|
|
||||||
export const FSharp: Info = {
|
export const FSharp: Info = {
|
||||||
id: "fsharp",
|
id: "fsharp",
|
||||||
root: NearestRoot([".sln", ".fsproj", "global.json"]),
|
root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
|
||||||
extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
|
extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let bin = Bun.which("fsautocomplete", {
|
let bin = Bun.which("fsautocomplete", {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
import os from "os"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
|
import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
|
||||||
@@ -35,8 +36,9 @@ import { createGateway } from "@ai-sdk/gateway"
|
|||||||
import { createTogetherAI } from "@ai-sdk/togetherai"
|
import { createTogetherAI } from "@ai-sdk/togetherai"
|
||||||
import { createPerplexity } from "@ai-sdk/perplexity"
|
import { createPerplexity } from "@ai-sdk/perplexity"
|
||||||
import { createVercel } from "@ai-sdk/vercel"
|
import { createVercel } from "@ai-sdk/vercel"
|
||||||
import { createGitLab } from "@gitlab/gitlab-ai-provider"
|
import { createGitLab, VERSION as GITLAB_PROVIDER_VERSION } from "@gitlab/gitlab-ai-provider"
|
||||||
import { ProviderTransform } from "./transform"
|
import { ProviderTransform } from "./transform"
|
||||||
|
import { Installation } from "../installation"
|
||||||
|
|
||||||
export namespace Provider {
|
export namespace Provider {
|
||||||
const log = Log.create({ service: "provider" })
|
const log = Log.create({ service: "provider" })
|
||||||
@@ -237,7 +239,9 @@ export namespace Provider {
|
|||||||
options: providerOptions,
|
options: providerOptions,
|
||||||
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
||||||
// Skip region prefixing if model already has a cross-region inference profile prefix
|
// Skip region prefixing if model already has a cross-region inference profile prefix
|
||||||
if (modelID.startsWith("global.") || modelID.startsWith("jp.")) {
|
// Models from models.dev may already include prefixes like us., eu., global., etc.
|
||||||
|
const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
|
||||||
|
if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) {
|
||||||
return sdk.languageModel(modelID)
|
return sdk.languageModel(modelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,11 +428,17 @@ export namespace Provider {
|
|||||||
const config = await Config.get()
|
const config = await Config.get()
|
||||||
const providerConfig = config.provider?.["gitlab"]
|
const providerConfig = config.provider?.["gitlab"]
|
||||||
|
|
||||||
|
const aiGatewayHeaders = {
|
||||||
|
"User-Agent": `opencode/${Installation.VERSION} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||||
|
...(providerConfig?.options?.aiGatewayHeaders || {}),
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
autoload: !!apiKey,
|
autoload: !!apiKey,
|
||||||
options: {
|
options: {
|
||||||
instanceUrl,
|
instanceUrl,
|
||||||
apiKey,
|
apiKey,
|
||||||
|
aiGatewayHeaders,
|
||||||
featureFlags: {
|
featureFlags: {
|
||||||
duo_agent_platform_agentic_chat: true,
|
duo_agent_platform_agentic_chat: true,
|
||||||
duo_agent_platform: true,
|
duo_agent_platform: true,
|
||||||
@@ -437,6 +447,7 @@ export namespace Provider {
|
|||||||
},
|
},
|
||||||
async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string) {
|
async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string) {
|
||||||
return sdk.agenticChat(modelID, {
|
return sdk.agenticChat(modelID, {
|
||||||
|
aiGatewayHeaders,
|
||||||
featureFlags: {
|
featureFlags: {
|
||||||
duo_agent_platform_agentic_chat: true,
|
duo_agent_platform_agentic_chat: true,
|
||||||
duo_agent_platform: true,
|
duo_agent_platform: true,
|
||||||
@@ -452,52 +463,36 @@ export namespace Provider {
|
|||||||
|
|
||||||
if (!accountId || !gateway) return { autoload: false }
|
if (!accountId || !gateway) return { autoload: false }
|
||||||
|
|
||||||
// Get API token from env or auth prompt
|
// Get API token from env or auth - required for authenticated gateways
|
||||||
const apiToken = await (async () => {
|
const apiToken = await (async () => {
|
||||||
const envToken = Env.get("CLOUDFLARE_API_TOKEN")
|
const envToken = Env.get("CLOUDFLARE_API_TOKEN") || Env.get("CF_AIG_TOKEN")
|
||||||
if (envToken) return envToken
|
if (envToken) return envToken
|
||||||
const auth = await Auth.get(input.id)
|
const auth = await Auth.get(input.id)
|
||||||
if (auth?.type === "api") return auth.key
|
if (auth?.type === "api") return auth.key
|
||||||
return undefined
|
return undefined
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
if (!apiToken) {
|
||||||
|
throw new Error(
|
||||||
|
"CLOUDFLARE_API_TOKEN (or CF_AIG_TOKEN) is required for Cloudflare AI Gateway. " +
|
||||||
|
"Set it via environment variable or run `opencode auth cloudflare-ai-gateway`.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use official ai-gateway-provider package (v2.x for AI SDK v5 compatibility)
|
||||||
|
const { createAiGateway } = await import("ai-gateway-provider")
|
||||||
|
const { createUnified } = await import("ai-gateway-provider/providers/unified")
|
||||||
|
|
||||||
|
const aigateway = createAiGateway({ accountId, gateway, apiKey: apiToken })
|
||||||
|
const unified = createUnified()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
autoload: true,
|
autoload: true,
|
||||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
async getModel(_sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||||
return sdk.languageModel(modelID)
|
// Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5")
|
||||||
},
|
return aigateway(unified(modelID))
|
||||||
options: {
|
|
||||||
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gateway}/compat`,
|
|
||||||
headers: {
|
|
||||||
// Cloudflare AI Gateway uses cf-aig-authorization for authenticated gateways
|
|
||||||
// This enables Unified Billing where Cloudflare handles upstream provider auth
|
|
||||||
...(apiToken ? { "cf-aig-authorization": `Bearer ${apiToken}` } : {}),
|
|
||||||
"HTTP-Referer": "https://opencode.ai/",
|
|
||||||
"X-Title": "opencode",
|
|
||||||
},
|
|
||||||
// Custom fetch to handle parameter transformation and auth
|
|
||||||
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
||||||
const headers = new Headers(init?.headers)
|
|
||||||
// Strip Authorization header - AI Gateway uses cf-aig-authorization instead
|
|
||||||
headers.delete("Authorization")
|
|
||||||
|
|
||||||
// Transform max_tokens to max_completion_tokens for newer models
|
|
||||||
if (init?.body && init.method === "POST") {
|
|
||||||
try {
|
|
||||||
const body = JSON.parse(init.body as string)
|
|
||||||
if (body.max_tokens !== undefined && !body.max_completion_tokens) {
|
|
||||||
body.max_completion_tokens = body.max_tokens
|
|
||||||
delete body.max_tokens
|
|
||||||
init = { ...init, body: JSON.stringify(body) }
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// If body parsing fails, continue with original request
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch(input, { ...init, headers })
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
options: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cerebras: async () => {
|
cerebras: async () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { APICallError, ModelMessage } from "ai"
|
import type { APICallError, ModelMessage } from "ai"
|
||||||
import { mergeDeep, unique } from "remeda"
|
import { mergeDeep, unique } from "remeda"
|
||||||
|
import type { JSONSchema7 } from "@ai-sdk/provider"
|
||||||
import type { JSONSchema } from "zod/v4/core"
|
import type { JSONSchema } from "zod/v4/core"
|
||||||
import type { Provider } from "./provider"
|
import type { Provider } from "./provider"
|
||||||
import type { ModelsDev } from "./models"
|
import type { ModelsDev } from "./models"
|
||||||
@@ -333,7 +334,9 @@ export namespace ProviderTransform {
|
|||||||
id.includes("minimax") ||
|
id.includes("minimax") ||
|
||||||
id.includes("glm") ||
|
id.includes("glm") ||
|
||||||
id.includes("mistral") ||
|
id.includes("mistral") ||
|
||||||
id.includes("kimi")
|
id.includes("kimi") ||
|
||||||
|
// TODO: Remove this after models.dev data is fixed to use "kimi-k2.5" instead of "k2p5"
|
||||||
|
id.includes("k2p5")
|
||||||
)
|
)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -717,7 +720,7 @@ export namespace ProviderTransform {
|
|||||||
return standardLimit
|
return standardLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema) {
|
export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JSONSchema7): JSONSchema7 {
|
||||||
/*
|
/*
|
||||||
if (["openai", "azure"].includes(providerID)) {
|
if (["openai", "azure"].includes(providerID)) {
|
||||||
if (schema.type === "object" && schema.properties) {
|
if (schema.type === "object" && schema.properties) {
|
||||||
@@ -768,8 +771,21 @@ export namespace ProviderTransform {
|
|||||||
result.required = result.required.filter((field: any) => field in result.properties)
|
result.required = result.required.filter((field: any) => field in result.properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.type === "array" && result.items == null) {
|
if (result.type === "array") {
|
||||||
result.items = {}
|
if (result.items == null) {
|
||||||
|
result.items = {}
|
||||||
|
}
|
||||||
|
// Ensure items has at least a type if it's an empty object
|
||||||
|
// This handles nested arrays like { type: "array", items: { type: "array", items: {} } }
|
||||||
|
if (typeof result.items === "object" && !Array.isArray(result.items) && !result.items.type) {
|
||||||
|
result.items.type = "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove properties/required from non-object types (Gemini rejects these)
|
||||||
|
if (result.type && result.type !== "object") {
|
||||||
|
delete result.properties
|
||||||
|
delete result.required
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -778,7 +794,7 @@ export namespace ProviderTransform {
|
|||||||
schema = sanitizeGemini(schema)
|
schema = sanitizeGemini(schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
return schema
|
return schema as JSONSchema7
|
||||||
}
|
}
|
||||||
|
|
||||||
export function error(providerID: string, error: APICallError) {
|
export function error(providerID: string, error: APICallError) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { WSContext } from "hono/ws"
|
|||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { lazy } from "@opencode-ai/util/lazy"
|
import { lazy } from "@opencode-ai/util/lazy"
|
||||||
import { Shell } from "@/shell/shell"
|
import { Shell } from "@/shell/shell"
|
||||||
|
import { Plugin } from "@/plugin"
|
||||||
|
|
||||||
export namespace Pty {
|
export namespace Pty {
|
||||||
const log = Log.create({ service: "pty" })
|
const log = Log.create({ service: "pty" })
|
||||||
@@ -102,9 +103,11 @@ export namespace Pty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cwd = input.cwd || Instance.directory
|
const cwd = input.cwd || Instance.directory
|
||||||
|
const shellEnv = await Plugin.trigger("shell.env", { cwd }, { env: {} })
|
||||||
const env = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
...input.env,
|
...input.env,
|
||||||
|
...shellEnv.env,
|
||||||
TERM: "xterm-256color",
|
TERM: "xterm-256color",
|
||||||
OPENCODE_TERMINAL: "1",
|
OPENCODE_TERMINAL: "1",
|
||||||
} as Record<string, string>
|
} as Record<string, string>
|
||||||
|
|||||||
@@ -7,17 +7,18 @@ export namespace MDNS {
|
|||||||
let bonjour: Bonjour | undefined
|
let bonjour: Bonjour | undefined
|
||||||
let currentPort: number | undefined
|
let currentPort: number | undefined
|
||||||
|
|
||||||
export function publish(port: number) {
|
export function publish(port: number, domain?: string) {
|
||||||
if (currentPort === port) return
|
if (currentPort === port) return
|
||||||
if (bonjour) unpublish()
|
if (bonjour) unpublish()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const host = domain ?? "opencode.local"
|
||||||
const name = `opencode-${port}`
|
const name = `opencode-${port}`
|
||||||
bonjour = new Bonjour()
|
bonjour = new Bonjour()
|
||||||
const service = bonjour.publish({
|
const service = bonjour.publish({
|
||||||
name,
|
name,
|
||||||
type: "http",
|
type: "http",
|
||||||
host: "opencode.local",
|
host,
|
||||||
port,
|
port,
|
||||||
txt: { path: "/" },
|
txt: { path: "/" },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -563,7 +563,13 @@ export namespace Server {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listen(opts: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
|
export function listen(opts: {
|
||||||
|
port: number
|
||||||
|
hostname: string
|
||||||
|
mdns?: boolean
|
||||||
|
mdnsDomain?: string
|
||||||
|
cors?: string[]
|
||||||
|
}) {
|
||||||
_corsWhitelist = opts.cors ?? []
|
_corsWhitelist = opts.cors ?? []
|
||||||
|
|
||||||
const args = {
|
const args = {
|
||||||
@@ -591,7 +597,7 @@ export namespace Server {
|
|||||||
opts.hostname !== "localhost" &&
|
opts.hostname !== "localhost" &&
|
||||||
opts.hostname !== "::1"
|
opts.hostname !== "::1"
|
||||||
if (shouldPublishMDNS) {
|
if (shouldPublishMDNS) {
|
||||||
MDNS.publish(server.port!)
|
MDNS.publish(server.port!, opts.mdnsDomain)
|
||||||
} else if (opts.mdns) {
|
} else if (opts.mdns) {
|
||||||
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
|
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ const FILES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
function globalFiles() {
|
function globalFiles() {
|
||||||
const files = [path.join(Global.Path.config, "AGENTS.md")]
|
const files = []
|
||||||
if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
|
|
||||||
files.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
|
|
||||||
}
|
|
||||||
if (Flag.OPENCODE_CONFIG_DIR) {
|
if (Flag.OPENCODE_CONFIG_DIR) {
|
||||||
files.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))
|
files.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))
|
||||||
}
|
}
|
||||||
|
files.push(path.join(Global.Path.config, "AGENTS.md"))
|
||||||
|
if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
|
||||||
|
files.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
|
||||||
|
}
|
||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,14 +172,6 @@ export namespace SessionProcessor {
|
|||||||
case "tool-result": {
|
case "tool-result": {
|
||||||
const match = toolcalls[value.toolCallId]
|
const match = toolcalls[value.toolCallId]
|
||||||
if (match && match.state.status === "running") {
|
if (match && match.state.status === "running") {
|
||||||
const attachments = value.output.attachments?.map(
|
|
||||||
(attachment: Omit<MessageV2.FilePart, "id" | "messageID" | "sessionID">) => ({
|
|
||||||
...attachment,
|
|
||||||
id: Identifier.ascending("part"),
|
|
||||||
messageID: match.messageID,
|
|
||||||
sessionID: match.sessionID,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
await Session.updatePart({
|
await Session.updatePart({
|
||||||
...match,
|
...match,
|
||||||
state: {
|
state: {
|
||||||
@@ -192,7 +184,7 @@ export namespace SessionProcessor {
|
|||||||
start: match.state.time.start,
|
start: match.state.time.start,
|
||||||
end: Date.now(),
|
end: Date.now(),
|
||||||
},
|
},
|
||||||
attachments,
|
attachments: value.output.attachments,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { SessionRevert } from "./revert"
|
|||||||
import { Session } from "."
|
import { Session } from "."
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent/agent"
|
||||||
import { Provider } from "../provider/provider"
|
import { Provider } from "../provider/provider"
|
||||||
import { type Tool as AITool, tool, jsonSchema, type ToolCallOptions } from "ai"
|
import { type Tool as AITool, tool, jsonSchema, type ToolCallOptions, asSchema } from "ai"
|
||||||
import { SessionCompaction } from "./compaction"
|
import { SessionCompaction } from "./compaction"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
@@ -187,17 +187,13 @@ export namespace SessionPrompt {
|
|||||||
text: template,
|
text: template,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
const matches = ConfigMarkdown.files(template)
|
const files = ConfigMarkdown.files(template)
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const names = matches
|
await Promise.all(
|
||||||
.map((match) => match[1])
|
files.map(async (match) => {
|
||||||
.filter((name) => {
|
const name = match[1]
|
||||||
if (seen.has(name)) return false
|
if (seen.has(name)) return
|
||||||
seen.add(name)
|
seen.add(name)
|
||||||
return true
|
|
||||||
})
|
|
||||||
const resolved = await Promise.all(
|
|
||||||
names.map(async (name) => {
|
|
||||||
const filepath = name.startsWith("~/")
|
const filepath = name.startsWith("~/")
|
||||||
? path.join(os.homedir(), name.slice(2))
|
? path.join(os.homedir(), name.slice(2))
|
||||||
: path.resolve(Instance.worktree, name)
|
: path.resolve(Instance.worktree, name)
|
||||||
@@ -205,34 +201,33 @@ export namespace SessionPrompt {
|
|||||||
const stats = await fs.stat(filepath).catch(() => undefined)
|
const stats = await fs.stat(filepath).catch(() => undefined)
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
const agent = await Agent.get(name)
|
const agent = await Agent.get(name)
|
||||||
if (!agent) return undefined
|
if (agent) {
|
||||||
return {
|
parts.push({
|
||||||
type: "agent",
|
type: "agent",
|
||||||
name: agent.name,
|
name: agent.name,
|
||||||
} satisfies PromptInput["parts"][number]
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
return {
|
parts.push({
|
||||||
type: "file",
|
type: "file",
|
||||||
url: `file://${filepath}`,
|
url: `file://${filepath}`,
|
||||||
filename: name,
|
filename: name,
|
||||||
mime: "application/x-directory",
|
mime: "application/x-directory",
|
||||||
} satisfies PromptInput["parts"][number]
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
parts.push({
|
||||||
type: "file",
|
type: "file",
|
||||||
url: `file://${filepath}`,
|
url: `file://${filepath}`,
|
||||||
filename: name,
|
filename: name,
|
||||||
mime: "text/plain",
|
mime: "text/plain",
|
||||||
} satisfies PromptInput["parts"][number]
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
for (const item of resolved) {
|
|
||||||
if (!item) continue
|
|
||||||
parts.push(item)
|
|
||||||
}
|
|
||||||
return parts
|
return parts
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,12 +427,6 @@ export namespace SessionPrompt {
|
|||||||
assistantMessage.time.completed = Date.now()
|
assistantMessage.time.completed = Date.now()
|
||||||
await Session.updateMessage(assistantMessage)
|
await Session.updateMessage(assistantMessage)
|
||||||
if (result && part.state.status === "running") {
|
if (result && part.state.status === "running") {
|
||||||
const attachments = result.attachments?.map((attachment) => ({
|
|
||||||
...attachment,
|
|
||||||
id: Identifier.ascending("part"),
|
|
||||||
messageID: assistantMessage.id,
|
|
||||||
sessionID: assistantMessage.sessionID,
|
|
||||||
}))
|
|
||||||
await Session.updatePart({
|
await Session.updatePart({
|
||||||
...part,
|
...part,
|
||||||
state: {
|
state: {
|
||||||
@@ -446,7 +435,7 @@ export namespace SessionPrompt {
|
|||||||
title: result.title,
|
title: result.title,
|
||||||
metadata: result.metadata,
|
metadata: result.metadata,
|
||||||
output: result.output,
|
output: result.output,
|
||||||
attachments,
|
attachments: result.attachments,
|
||||||
time: {
|
time: {
|
||||||
...part.state.time,
|
...part.state.time,
|
||||||
end: Date.now(),
|
end: Date.now(),
|
||||||
@@ -749,6 +738,8 @@ export namespace SessionPrompt {
|
|||||||
const execute = item.execute
|
const execute = item.execute
|
||||||
if (!execute) continue
|
if (!execute) continue
|
||||||
|
|
||||||
|
const transformed = ProviderTransform.schema(input.model, asSchema(item.inputSchema).jsonSchema)
|
||||||
|
item.inputSchema = jsonSchema(transformed)
|
||||||
// Wrap execute to add plugin hooks and format output
|
// Wrap execute to add plugin hooks and format output
|
||||||
item.execute = async (args, opts) => {
|
item.execute = async (args, opts) => {
|
||||||
const ctx = context(args, opts)
|
const ctx = context(args, opts)
|
||||||
@@ -785,13 +776,16 @@ export namespace SessionPrompt {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const textParts: string[] = []
|
const textParts: string[] = []
|
||||||
const attachments: Omit<MessageV2.FilePart, "id" | "messageID" | "sessionID">[] = []
|
const attachments: MessageV2.FilePart[] = []
|
||||||
|
|
||||||
for (const contentItem of result.content) {
|
for (const contentItem of result.content) {
|
||||||
if (contentItem.type === "text") {
|
if (contentItem.type === "text") {
|
||||||
textParts.push(contentItem.text)
|
textParts.push(contentItem.text)
|
||||||
} else if (contentItem.type === "image") {
|
} else if (contentItem.type === "image") {
|
||||||
attachments.push({
|
attachments.push({
|
||||||
|
id: Identifier.ascending("part"),
|
||||||
|
sessionID: input.session.id,
|
||||||
|
messageID: input.processor.message.id,
|
||||||
type: "file",
|
type: "file",
|
||||||
mime: contentItem.mimeType,
|
mime: contentItem.mimeType,
|
||||||
url: `data:${contentItem.mimeType};base64,${contentItem.data}`,
|
url: `data:${contentItem.mimeType};base64,${contentItem.data}`,
|
||||||
@@ -803,6 +797,9 @@ export namespace SessionPrompt {
|
|||||||
}
|
}
|
||||||
if (resource.blob) {
|
if (resource.blob) {
|
||||||
attachments.push({
|
attachments.push({
|
||||||
|
id: Identifier.ascending("part"),
|
||||||
|
sessionID: input.session.id,
|
||||||
|
messageID: input.processor.message.id,
|
||||||
type: "file",
|
type: "file",
|
||||||
mime: resource.mimeType ?? "application/octet-stream",
|
mime: resource.mimeType ?? "application/octet-stream",
|
||||||
url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`,
|
url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`,
|
||||||
@@ -1051,7 +1048,6 @@ export namespace SessionPrompt {
|
|||||||
pieces.push(
|
pieces.push(
|
||||||
...result.attachments.map((attachment) => ({
|
...result.attachments.map((attachment) => ({
|
||||||
...attachment,
|
...attachment,
|
||||||
id: Identifier.ascending("part"),
|
|
||||||
synthetic: true,
|
synthetic: true,
|
||||||
filename: attachment.filename ?? part.filename,
|
filename: attachment.filename ?? part.filename,
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
@@ -1189,18 +1185,7 @@ export namespace SessionPrompt {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
)
|
).then((x) => x.flat())
|
||||||
.then((x) => x.flat())
|
|
||||||
.then((drafts) =>
|
|
||||||
drafts.map(
|
|
||||||
(part): MessageV2.Part => ({
|
|
||||||
...part,
|
|
||||||
id: Identifier.ascending("part"),
|
|
||||||
messageID: info.id,
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
await Plugin.trigger(
|
await Plugin.trigger(
|
||||||
"chat.message",
|
"chat.message",
|
||||||
@@ -1515,12 +1500,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
const matchingInvocation = invocations[shellName] ?? invocations[""]
|
const matchingInvocation = invocations[shellName] ?? invocations[""]
|
||||||
const args = matchingInvocation?.args
|
const args = matchingInvocation?.args
|
||||||
|
|
||||||
|
const cwd = Instance.directory
|
||||||
|
const shellEnv = await Plugin.trigger("shell.env", { cwd }, { env: {} })
|
||||||
const proc = spawn(shell, args, {
|
const proc = spawn(shell, args, {
|
||||||
cwd: Instance.directory,
|
cwd,
|
||||||
detached: process.platform !== "win32",
|
detached: process.platform !== "win32",
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
...shellEnv.env,
|
||||||
TERM: "dumb",
|
TERM: "dumb",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -40,12 +40,17 @@ export namespace Skill {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// External skill directories to search for (project-level and global)
|
||||||
|
// These follow the directory layout used by Claude Code and other agents.
|
||||||
|
const EXTERNAL_DIRS = [".claude", ".agents"]
|
||||||
|
const EXTERNAL_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")
|
||||||
|
|
||||||
const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md")
|
const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md")
|
||||||
const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")
|
|
||||||
const SKILL_GLOB = new Bun.Glob("**/SKILL.md")
|
const SKILL_GLOB = new Bun.Glob("**/SKILL.md")
|
||||||
|
|
||||||
export const state = Instance.state(async () => {
|
export const state = Instance.state(async () => {
|
||||||
const skills: Record<string, Info> = {}
|
const skills: Record<string, Info> = {}
|
||||||
|
const dirs = new Set<string>()
|
||||||
|
|
||||||
const addSkill = async (match: string) => {
|
const addSkill = async (match: string) => {
|
||||||
const md = await ConfigMarkdown.parse(match).catch((err) => {
|
const md = await ConfigMarkdown.parse(match).catch((err) => {
|
||||||
@@ -71,6 +76,8 @@ export namespace Skill {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dirs.add(path.dirname(match))
|
||||||
|
|
||||||
skills[parsed.data.name] = {
|
skills[parsed.data.name] = {
|
||||||
name: parsed.data.name,
|
name: parsed.data.name,
|
||||||
description: parsed.data.description,
|
description: parsed.data.description,
|
||||||
@@ -79,38 +86,37 @@ export namespace Skill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan .claude/skills/ directories (project-level)
|
const scanExternal = async (root: string, scope: "global" | "project") => {
|
||||||
const claudeDirs = await Array.fromAsync(
|
return Array.fromAsync(
|
||||||
Filesystem.up({
|
EXTERNAL_SKILL_GLOB.scan({
|
||||||
targets: [".claude"],
|
cwd: root,
|
||||||
start: Instance.directory,
|
absolute: true,
|
||||||
stop: Instance.worktree,
|
onlyFiles: true,
|
||||||
}),
|
followSymlinks: true,
|
||||||
)
|
dot: true,
|
||||||
// Also include global ~/.claude/skills/
|
}),
|
||||||
const globalClaude = `${Global.Path.home}/.claude`
|
)
|
||||||
if (await Filesystem.isDir(globalClaude)) {
|
.then((matches) => Promise.all(matches.map(addSkill)))
|
||||||
claudeDirs.push(globalClaude)
|
.catch((error) => {
|
||||||
|
log.error(`failed to scan ${scope} skills`, { dir: root, error })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS) {
|
// Scan external skill directories (.claude/skills/, .agents/skills/, etc.)
|
||||||
for (const dir of claudeDirs) {
|
// Load global (home) first, then project-level (so project-level overwrites)
|
||||||
const matches = await Array.fromAsync(
|
if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {
|
||||||
CLAUDE_SKILL_GLOB.scan({
|
for (const dir of EXTERNAL_DIRS) {
|
||||||
cwd: dir,
|
const root = path.join(Global.Path.home, dir)
|
||||||
absolute: true,
|
if (!(await Filesystem.isDir(root))) continue
|
||||||
onlyFiles: true,
|
await scanExternal(root, "global")
|
||||||
followSymlinks: true,
|
}
|
||||||
dot: true,
|
|
||||||
}),
|
|
||||||
).catch((error) => {
|
|
||||||
log.error("failed .claude directory scan for skills", { dir, error })
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const match of matches) {
|
for await (const root of Filesystem.up({
|
||||||
await addSkill(match)
|
targets: EXTERNAL_DIRS,
|
||||||
}
|
start: Instance.directory,
|
||||||
|
stop: Instance.worktree,
|
||||||
|
})) {
|
||||||
|
await scanExternal(root, "project")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,14 +151,21 @@ export namespace Skill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return skills
|
return {
|
||||||
|
skills,
|
||||||
|
dirs: Array.from(dirs),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function get(name: string) {
|
export async function get(name: string) {
|
||||||
return state().then((x) => x[name])
|
return state().then((x) => x.skills[name])
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function all() {
|
export async function all() {
|
||||||
return state().then((x) => Object.values(x))
|
return state().then((x) => Object.values(x.skills))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dirs() {
|
||||||
|
return state().then((x) => x.dirs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export namespace Snapshot {
|
|||||||
after: z.string(),
|
after: z.string(),
|
||||||
additions: z.number(),
|
additions: z.number(),
|
||||||
deletions: z.number(),
|
deletions: z.number(),
|
||||||
|
status: z.enum(["added", "deleted", "modified"]).optional(),
|
||||||
})
|
})
|
||||||
.meta({
|
.meta({
|
||||||
ref: "FileDiff",
|
ref: "FileDiff",
|
||||||
@@ -196,6 +197,23 @@ export namespace Snapshot {
|
|||||||
export async function diffFull(from: string, to: string): Promise<FileDiff[]> {
|
export async function diffFull(from: string, to: string): Promise<FileDiff[]> {
|
||||||
const git = gitdir()
|
const git = gitdir()
|
||||||
const result: FileDiff[] = []
|
const result: FileDiff[] = []
|
||||||
|
const status = new Map<string, "added" | "deleted" | "modified">()
|
||||||
|
|
||||||
|
const statuses =
|
||||||
|
await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-status --no-renames ${from} ${to} -- .`
|
||||||
|
.quiet()
|
||||||
|
.cwd(Instance.directory)
|
||||||
|
.nothrow()
|
||||||
|
.text()
|
||||||
|
|
||||||
|
for (const line of statuses.trim().split("\n")) {
|
||||||
|
if (!line) continue
|
||||||
|
const [code, file] = line.split("\t")
|
||||||
|
if (!code || !file) continue
|
||||||
|
const kind = code.startsWith("A") ? "added" : code.startsWith("D") ? "deleted" : "modified"
|
||||||
|
status.set(file, kind)
|
||||||
|
}
|
||||||
|
|
||||||
for await (const line of $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`
|
for await (const line of $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`
|
||||||
.quiet()
|
.quiet()
|
||||||
.cwd(Instance.directory)
|
.cwd(Instance.directory)
|
||||||
@@ -224,6 +242,7 @@ export namespace Snapshot {
|
|||||||
after,
|
after,
|
||||||
additions: Number.isFinite(added) ? added : 0,
|
additions: Number.isFinite(added) ? added : 0,
|
||||||
deletions: Number.isFinite(deleted) ? deleted : 0,
|
deletions: Number.isFinite(deleted) ? deleted : 0,
|
||||||
|
status: status.get(file) ?? "modified",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Shell } from "@/shell/shell"
|
|||||||
|
|
||||||
import { BashArity } from "@/permission/arity"
|
import { BashArity } from "@/permission/arity"
|
||||||
import { Truncate } from "./truncation"
|
import { Truncate } from "./truncation"
|
||||||
|
import { Plugin } from "@/plugin"
|
||||||
|
|
||||||
const MAX_METADATA_LENGTH = 30_000
|
const MAX_METADATA_LENGTH = 30_000
|
||||||
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
|
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
|
||||||
@@ -128,7 +129,10 @@ export const BashTool = Tool.define("bash", async () => {
|
|||||||
process.platform === "win32" && resolved.match(/^\/[a-z]\//)
|
process.platform === "win32" && resolved.match(/^\/[a-z]\//)
|
||||||
? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\")
|
? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\")
|
||||||
: resolved
|
: resolved
|
||||||
if (!Instance.containsPath(normalized)) directories.add(normalized)
|
if (!Instance.containsPath(normalized)) {
|
||||||
|
const dir = (await Filesystem.isDir(normalized)) ? normalized : path.dirname(normalized)
|
||||||
|
directories.add(dir)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,10 +145,11 @@ export const BashTool = Tool.define("bash", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (directories.size > 0) {
|
if (directories.size > 0) {
|
||||||
|
const globs = Array.from(directories).map((dir) => path.join(dir, "*"))
|
||||||
await ctx.ask({
|
await ctx.ask({
|
||||||
permission: "external_directory",
|
permission: "external_directory",
|
||||||
patterns: Array.from(directories),
|
patterns: globs,
|
||||||
always: Array.from(directories).map((x) => path.dirname(x) + "*"),
|
always: globs,
|
||||||
metadata: {},
|
metadata: {},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -158,11 +163,13 @@ export const BashTool = Tool.define("bash", async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shellEnv = await Plugin.trigger("shell.env", { cwd }, { env: {} })
|
||||||
const proc = spawn(params.command, {
|
const proc = spawn(params.command, {
|
||||||
shell,
|
shell,
|
||||||
cwd,
|
cwd,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
...shellEnv.env,
|
||||||
},
|
},
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
detached: process.platform !== "win32",
|
detached: process.platform !== "win32",
|
||||||
|
|||||||
@@ -77,12 +77,6 @@ export const BatchTool = Tool.define("batch", async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const result = await tool.execute(validatedParams, { ...ctx, callID: partID })
|
const result = await tool.execute(validatedParams, { ...ctx, callID: partID })
|
||||||
const attachments = result.attachments?.map((attachment) => ({
|
|
||||||
...attachment,
|
|
||||||
id: Identifier.ascending("part"),
|
|
||||||
messageID: ctx.messageID,
|
|
||||||
sessionID: ctx.sessionID,
|
|
||||||
}))
|
|
||||||
|
|
||||||
await Session.updatePart({
|
await Session.updatePart({
|
||||||
id: partID,
|
id: partID,
|
||||||
@@ -97,7 +91,7 @@ export const BatchTool = Tool.define("batch", async () => {
|
|||||||
output: result.output,
|
output: result.output,
|
||||||
title: result.title,
|
title: result.title,
|
||||||
metadata: result.metadata,
|
metadata: result.metadata,
|
||||||
attachments,
|
attachments: result.attachments,
|
||||||
time: {
|
time: {
|
||||||
start: callStartTime,
|
start: callStartTime,
|
||||||
end: Date.now(),
|
end: Date.now(),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import DESCRIPTION from "./codesearch.txt"
|
import DESCRIPTION from "./codesearch.txt"
|
||||||
|
import { abortAfterAny } from "../util/abort"
|
||||||
|
|
||||||
const API_CONFIG = {
|
const API_CONFIG = {
|
||||||
BASE_URL: "https://mcp.exa.ai",
|
BASE_URL: "https://mcp.exa.ai",
|
||||||
@@ -73,8 +74,7 @@ export const CodeSearchTool = Tool.define("codesearch", {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = new AbortController()
|
const { signal, clearTimeout } = abortAfterAny(30000, ctx.abort)
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 30000)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
@@ -86,10 +86,10 @@ export const CodeSearchTool = Tool.define("codesearch", {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(codeRequest),
|
body: JSON.stringify(codeRequest),
|
||||||
signal: AbortSignal.any([controller.signal, ctx.abort]),
|
signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
clearTimeout(timeoutId)
|
clearTimeout()
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text()
|
const errorText = await response.text()
|
||||||
@@ -120,7 +120,7 @@ export const CodeSearchTool = Tool.define("codesearch", {
|
|||||||
metadata: {},
|
metadata: {},
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearTimeout(timeoutId)
|
clearTimeout()
|
||||||
|
|
||||||
if (error instanceof Error && error.name === "AbortError") {
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
throw new Error("Code search request timed out")
|
throw new Error("Code search request timed out")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { LSP } from "../lsp"
|
|||||||
import { FileTime } from "../file/time"
|
import { FileTime } from "../file/time"
|
||||||
import DESCRIPTION from "./read.txt"
|
import DESCRIPTION from "./read.txt"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
|
import { Identifier } from "../id/id"
|
||||||
import { assertExternalDirectory } from "./external-directory"
|
import { assertExternalDirectory } from "./external-directory"
|
||||||
import { InstructionPrompt } from "../session/instruction"
|
import { InstructionPrompt } from "../session/instruction"
|
||||||
|
|
||||||
@@ -78,6 +79,9 @@ export const ReadTool = Tool.define("read", {
|
|||||||
},
|
},
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
|
id: Identifier.ascending("part"),
|
||||||
|
sessionID: ctx.sessionID,
|
||||||
|
messageID: ctx.messageID,
|
||||||
type: "file",
|
type: "file",
|
||||||
mime,
|
mime,
|
||||||
url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString("base64")}`,
|
url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString("base64")}`,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user