Compare commits

...
Author SHA1 Message Date
Dax Raad 440eb76e52 fix(config): install plugin deps per config directory 2026-02-05 16:22:13 -05:00
Aiden Cline b551195818 tweak: move codex 5.3 model definition to the plugin so that models.dev doesnt have to show unsupported model for others (#12344) 2026-02-05 20:53:26 +00:00
Adam d7c2d5db3b fix(app): hide prompt input when there are perms or questions (#12339) 2026-02-05 14:42:56 -06:00
opencode-agent[bot] 1dd88aeae6 chore: update nix node_modules hashes 2026-02-05 20:31:07 +00:00
Adam 2875405514 fix(app): more terminal stability fixes 2026-02-05 14:21:13 -06:00
Adam 8c0300c021 fix(app): modified file color contrast 2026-02-05 14:21:13 -06:00
Adam 26b786dd3f fix(ui): toast overflow 2026-02-05 14:21:12 -06:00
opencode-agent[bot] afce869d3b chore: generate 2026-02-05 19:56:54 +00:00
Adam b738d88ec4 feat(app): open in <app> button (#12322) 2026-02-05 19:55:49 +00:00
Adam 83646e0366 fix(app): allow toggling file tree closed independently (#12293) 2026-02-05 13:51:08 -06:00
Daniel Polito c40ce47e92 feat(desktop): Stop Showing SessionSkeleton on New Workspace (#12209) 2026-02-05 13:46:44 -06:00
Daniel Polito b1c44c7e5c feat(desktop): Set Workspace Name Earlier to Improve Creation / Deletion (#12213) 2026-02-05 13:38:31 -06:00
opencode-agent[bot] 081f065942 chore: update nix node_modules hashes 2026-02-05 19:09:43 +00:00
Steffen DeuschandAiden Cline 8ddef975b7 feat(acp): add session usage (#12299)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-02-05 12:58:09 -06:00
Aiden Cline 2f78705f6e tweak: update transforms for gpt-5.3 (#12325) 2026-02-05 12:49:40 -06:00
opencode a0bc656215 release: v1.1.52 2026-02-05 18:48:54 +00:00
Dax Raad 47f00d23b3 enable 5.3 codex 2026-02-05 13:23:58 -05:00
Goni Zahavy 40ebc34909 feat(tui): add running spinner to bash tool in TUI (#12317) 2026-02-05 12:23:02 -06:00
Frank e08705f4ef zen: opus 4.6 2026-02-05 13:01:50 -05:00
Dax Raad 7c748ef089 core: silently ignore proxy command failures to prevent config initialization crashes 2026-02-05 12:47:32 -05:00
Aiden Cline fba5a79c45 fix: ensure that github copilot plugin properly sets headers when used in other clients than tui (#12316) 2026-02-05 11:40:29 -06:00
opencode-agent[bot] dbde377ab0 chore: update nix node_modules hashes 2026-02-05 16:45:41 +00:00
Dax Raad 9adcf524e2 core: bundle GitLab auth plugin directly instead of dynamic install
Remove dynamic installation of built-in plugins. GitLab auth is now imported

directly as an internal plugin, eliminating network requests during startup

and simplifying the plugin loading logic.

Removes the need for test mocks since plugins are no longer

dynamically installed at runtime.
2026-02-05 11:29:00 -05:00
Dax Raad 531b1941a0 ci: ensure config test waits for dependencies to complete installation 2026-02-05 10:13:52 -05:00
Dax Raad a1c46e05ee core: fix plugin installation to use direct package.json manipulation instead of bun add
This ensures plugins install more reliably by writing dependencies directly
to package.json rather than relying on bun add commands which can fail
in certain environments. Also adds a small delay to ensure filesystem
operations complete before proceeding.
2026-02-05 10:07:59 -05:00
Adam 1fe1457cfa chore: cleanup 2026-02-05 07:21:50 -06:00
Adam aedd85d885 fix(app): file changes not always available 2026-02-05 07:04:44 -06:00
Adam 5b3d94ebaa fix(app): file tree out of sync 2026-02-05 07:04:43 -06:00
Adam 1a6a3f4b54 chore: package.json scripts 2026-02-05 07:04:34 -06:00
Adam 3116cfc167 chore: package.json scripts 2026-02-05 07:03:29 -06:00
Adam 05529f66d7 fix(app): copy buttons smaller and out of the way 2026-02-05 07:03:29 -06:00
Edin ef09dddaa5 feat(i18n): add Bosnian locale (#12283) 2026-02-05 06:58:14 -06:00
Adam bf7af99a3f fix(app): terminal URL issues 2026-02-05 06:20:19 -06:00
Adam 7555742bf0 chore: cleanup 2026-02-05 06:04:28 -06:00
Brendan Allan fa20bc296b app: remove extra x padding around prompt input on mobile 2026-02-05 16:42:05 +08:00
opencode-agent[bot] 195731f347 chore: update nix node_modules hashes 2026-02-05 08:00:44 +00:00
72de9fe7a6 fix(opencode): Fixes image reading with OpenAI-compatible providers like Kimi K2.5. (#11323)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-02-05 01:54:07 -06:00
Aiden Cline bec1148192 fix: downgrade xai ai-sdk package due to errors (#12251) 2026-02-05 01:50:05 -06:00
Aiden Cline 8c8d888140 Revert: feat: add models.dev schema ref for model autocomplete in ope… (#12242) 2026-02-05 00:31:08 -06:00
opencode-agent[bot] d3a247bfff chore: generate 2026-02-05 06:08:54 +00:00
Tommy D. Rossi 9ef319f25f feat: add models.dev schema ref for model autocomplete in opencode.json (#12112) 2026-02-05 00:08:08 -06:00
Aiden Cline 64e2bf8bf0 tweak: adjust task tool description/input to alleviate tool call failures that sometimes occured w/ gpt models (#12214) 2026-02-05 00:04:03 -06:00
Dax 556adad67b fix: wait for dependencies before loading custom tools and plugins (#12227) 2026-02-05 03:25:43 +00:00
Frank 843bbc973a wip: zen 2026-02-04 20:56:38 -05:00
Frank 173804c097 zen: set session affinity header 2026-02-04 20:56:37 -05:00
Filip 4086a9ae8e fix(app): refresh workspace sessions on project switch (#12189) 2026-02-04 19:33:34 -06:00
opencode-agent[bot] 2614342f97 chore: generate 2026-02-05 00:30:42 +00:00
Ariane Emory 4387f9fb9a feat: Allow the function to hide or show thinking blocks to be bound to a key (resolves #12168) (#12171) 2026-02-04 18:29:46 -06:00
Maharshi Patel 31e2feb347 fix(tui): add hover states to question tool tabs (#12203) 2026-02-04 18:28:22 -06:00
Adam 2896b8a863 fix(app): terminal url 2026-02-04 15:43:13 -06:00
Shantur Rathore 0d38e69038 fix(core): skip dependency install in read-only config dirs (#12128) 2026-02-04 14:45:59 -06:00
Adam 9679e0c59c fix(app): terminal EOL issues 2026-02-04 14:36:10 -06:00
Parker Smith 41bc4ec7f0 fix(desktop): Refresh file contents when changing workspaces to not have stale contents (#11728) 2026-02-04 14:27:02 -06:00
Dave Dennis 912098928a fix(app): derive terminal WebSocket URL from browser origin instead o… (#12178) 2026-02-04 14:22:50 -06:00
Adam 222bddc41a fix(app): last turn changes rendered in review pane (#12182) 2026-02-04 19:50:56 +00:00
Adam 9436cb575b fix(app): safety triangle for sidebar hover (#12179) 2026-02-04 19:25:55 +00:00
Tom d1686661c0 fix: ensure kimi-for-coding plan has thinking on by default for k2p5 (#12147) 2026-02-04 12:39:22 -06:00
Aiden Cline 305007aa0c fix: cloudflare workers ai provider (#12157) 2026-02-04 18:07:33 +00:00
Aiden Cline a2c28fc8d7 fix: ensure that plugin installs use --no-cache when using http proxy to prevent random hangs (see bun issue) (#12161) 2026-02-04 12:01:00 -06:00
Adam ce87121067 fix(app): clear comments on prompt submission (#12148) 2026-02-04 11:19:03 -06:00
Adam ecd7854853 test(app): fix e2e test action 2026-02-04 11:03:54 -06:00
Adam 57b8c62909 fix(app): terminal hyperlink clicks 2026-02-04 10:54:55 -06:00
Adam 28dc5de6a8 fix(ui): review comments z-index stacking 2026-02-04 10:35:11 -06:00
Adam c875a1fc90 test(app): fix e2e test action 2026-02-04 10:28:24 -06:00
Adam 1721c6efdf fix(core): session errors when attachment file not found 2026-02-04 10:21:03 -06:00
Adam 93592702c3 test(app): fix dated e2e tests 2026-02-04 10:10:28 -06:00
Adam 61d3f788b8 fix(app): don't show scroll-to-bottom unecessarily 2026-02-04 10:01:00 -06:00
Dax Raad a3b281b2f3 ci: remove source-based AUR package from publish script
Simplifies the release process by publishing only the binary package to AUR,

eliminating the need to maintain separate source and binary build configurations.
2026-02-04 10:31:21 -05:00
Adam c8622df762 fix(app): file tree not staying in sync across projects/sessions 2026-02-04 07:59:46 -06:00
Adam c277ee8cbf fix(app): move session options to the session page 2026-02-04 07:12:19 -06:00
Adam a2face30f4 wip(app): session options 2026-02-04 07:12:18 -06:00
Adam a219615fe5 fix(app): opened tabs follow created session 2026-02-04 06:22:55 -06:00
opencode-agent[bot] af06175b1f chore: generate 2026-02-04 11:35:05 +00:00
Goni Zahavy 2e8d8de58b fix(desktop): removed compression from rpm bundle to save 15m in CI (#12097) 2026-02-04 05:34:18 -06:00
opencode-agent[bot] 310de8b1ea chore: update nix node_modules hashes 2026-02-04 11:12:01 +00:00
Lucas (TaeYoung) Jo 891875402c fix(terminal): support remote server connections and fix GLIBC compatibility (#11906) 2026-02-04 05:01:10 -06:00
opencode 154cbf6996 release: v1.1.51 2026-02-04 07:06:48 +00:00
Sebastian 64bafce665 restore direct osc52 (#12071) 2026-02-04 00:35:57 -06:00
Aiden Cline 5588453cbe fix: revert change that caused headers to be double merged if provider was authed in multiple places (#12072) 2026-02-04 06:30:32 +00:00
Gary Chu 5aaf8f8247 docs: add agent-compatible paths to skills documentation (#12067) 2026-02-04 00:12:24 -06:00
Aiden Cline 8c1f1f13dc docs: document the built in agents (#12066) 2026-02-04 00:00:05 -06:00
Sergiy Dybskiy b942e0b4dc fix: prevent double-prefixing of Bedrock cross-region inference models (#12056) 2026-02-03 23:06:17 -06:00
Devin GriffinandBrendan Allan f282613746 fix(app): tighten up session padding-top for mobile (#11247)
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
2026-02-04 12:10:56 +08:00
Brendan Allan 7c440ae82c chore: add brendonovich as rust codeowner 2026-02-04 10:40:08 +08:00
Brendan Allan b7bd561eaa ci: use numeric release id instead of gql one 2026-02-04 10:39:17 +08:00
Łukasz Gładysz 6daa962aaa fix: prioritize OPENCODE_CONFIG_DIR for AGENTS.md (#11536) 2026-02-04 01:35:22 +00:00
opencode 93a07e5a2a release: v1.1.50 2026-02-04 01:35:15 +00:00
Max Leiter 93e060272a fix: prevent memory leaks from AbortController closures (#12024) 2026-02-03 17:51:26 -06:00
Filip acac05f22e refactor(e2e): faster tests (#12021) 2026-02-03 17:45:49 -06:00
Aiden Cline b5a4671c64 Revert "feat: add Trinity model system prompt support" (#12029) 2026-02-03 16:39:03 -06:00
Aiden Cline a68fedd4a6 chore: adjust way skill dirs are whitelisted (#12026) 2026-02-03 16:33:36 -06:00
Mariam JabaraandCursor 015cd404e4 feat: add Trinity model system prompt support (#12025)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 16:33:27 -06:00
opencode-agent[bot] 9921809565 chore: update nix node_modules hashes 2026-02-03 21:28:16 +00:00
opencode-agent[bot] 137336f373 chore: generate 2026-02-03 21:22:33 +00:00
Sayeed Mahmoud d940d17918 docs: fix grammar and formatting in README (#11985) 2026-02-03 15:21:51 -06:00
Tomáš Linhart 0a5d5bc524 docs: fix logging example for plugin (#11989) 2026-02-03 15:21:15 -06:00
Tyler Gannon a30696f9bf feat(plugin): add shell.env hook for manipulating environment in tools and shell (#12012) 2026-02-03 15:18:41 -06:00
Matt Silverlock 25bdd77b1d fix(opencode): use official ai-gateway-provider package for Cloudflare AI Gateway (#12014) 2026-02-03 15:18:28 -06:00
opencode-agent[bot] 2f12e8ee92 chore: generate 2026-02-03 21:10:08 +00:00
Idris Gadi 95211a8854 feat(tui): allow theme colors in agent customization (#11444) 2026-02-03 15:09:21 -06:00
Idris Gadi 6b5cf936a2 docs: add missing environmental flags to the list (#11146) 2026-02-03 15:07:40 -06:00
DaxandFilip 17e62b050f feat: add support for reading skills from .agents/skills directories (#11842)
Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com>
2026-02-03 15:51:54 -05:00
Dax ee84eb44ee cli: add --thinking flag to show reasoning blocks in run command (#12013) 2026-02-03 20:49:15 +00:00
MartinWie 82dd4b6908 fix: always fall back to native clipboard after OSC52 (#11994) 2026-02-03 20:53:04 +01:00
opencode-agent[bot] 185858749b chore: generate 2026-02-03 19:10:44 +00:00
Cloudyan 39a504773c fix: provider headers from config not applied to fetch requests (#11788) 2026-02-03 13:09:07 -06:00
Aiden Cline b7b734f51f fix: ensure mcp tools are sanitized (#11984) 2026-02-03 12:22:00 -06:00
Adam dcff5b6596 fix(app): command palette placeholder text 2026-02-03 11:58:58 -06:00
Adam 4f7da2b757 fix(app): model selector truncating too soon 2026-02-03 11:51:30 -06:00
Alex 60e616ec81 feat: Add .slnx to C#/F# LSP root detection (#11928) 2026-02-03 11:46:04 -06:00
Adam 416964acd0 chore: gitignore codex 2026-02-03 11:44:03 -06:00
Adam 017ad2c7f7 fix(app): spacing 2026-02-03 10:27:53 -06:00
Adam e33eb1b058 chore: cleanup 2026-02-03 10:27:53 -06:00
Aiden Cline 857b8a4b56 test: add test for new skill improvements (#11969) 2026-02-03 16:05:10 +00:00
Aiden Cline 3975329629 feat: improve skills, better prompting, fix permission asks after invoking skills, ensure agent knows where scripts/resources are (#11737) 2026-02-03 09:58:31 -06:00
neavo 54e14c1a17 fix: exclude k2p5 from reasoning variants (#11918) 2026-02-03 09:26:37 -06:00
Muhammad Mugni Hadi 3741516fe3 fix: handle nested array items for Gemini schema validation (#11952) 2026-02-03 09:24:52 -06:00
Daniel Polito 76381f33d5 feat(desktop): Allow empty prompt with review comments (#11953) 2026-02-03 09:06:02 -06:00
Luiz Guilherme D'Abruzzo Pereira 95d0d476e3 docs: add --mdns-domain flag documentation (#11933) 2026-02-03 07:26:42 -06:00
Adam 7508839b70 fix(app): terminal serialization bug 2026-02-03 07:03:34 -06:00
Adam e88cbefabe fix(app): terminal serialization bug 2026-02-03 07:03:34 -06:00
Adam a38bae684f chore(app): don't forceMount tooltips 2026-02-03 07:01:12 -06:00
Rahul A Mistry 08671e3155 fix(app): session tabs to open the previous opened (#11914) 2026-02-03 06:19:56 -06:00
Rahul A Mistry 0d557721cf fix(app): edit project dialog icon on hover (#11921) 2026-02-03 06:19:07 -06:00
Adam e709808b32 fix(app): move session search to command palette 2026-02-03 05:19:43 -06:00
Devin GriffinandBrendan Allan 0d22068c90 fix(app): custom providers overflow (#11252)
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
2026-02-03 16:17:13 +08:00
Filip d116c227e0 fix(core): plugins are always reinstalled (#9675) 2026-02-03 00:47:52 -06:00
opencode-agent[bot] 3f07dffbb0 chore: generate 2026-02-03 05:19:12 +00:00
Frank 801e4a8a9d wip: zen 2026-02-03 00:17:06 -05:00
Kiyoung Chang 3adeed8f97 fix(provider): strip properties/required from non-object types in Gemini schema (#11888) 2026-02-02 23:13:24 -06:00
Dax 1275c71a63 cli: make run non-interactive (#11814) 2026-02-02 23:35:23 -05:00
Aiden Cline ca8c23dd71 ci: add license to npm (#11883) 2026-02-03 03:44:30 +00:00
opencode acc2bf5db9 release: v1.1.49 2026-02-03 03:44:18 +00:00
Filip 96fbc30945 fix(app): drag region for native controls (#11854) 2026-02-02 18:20:05 -06:00
Alex Yaroshuk ba545ba9b3 fix(app): session scroll bar color (#11857) 2026-02-02 17:01:47 -06:00
Aiden Cline 188cc24bfc chore: cleanup external_dir perm logic (#11845) 2026-02-02 16:43:55 -06:00
opencode-agent[bot] 5e3162b7f4 chore: generate 2026-02-02 22:30:23 +00:00
Joseph Campuzano f9aa209131 fix: fixes issue where the autocomplete tui dialog flickers while typing (#11641) 2026-02-02 16:29:43 -06:00
Aiden Cline f86f654cda chore: rm dead code (#11849) 2026-02-02 16:22:03 -06:00
Filip aadd2e13d7 fix(app): prompt input overflow issue (#11840) 2026-02-02 22:02:56 +00:00
Adam 531357b40c fix(app): sidebar losing projects on collapse 2026-02-02 15:54:04 -06:00
Aiden Cline aa6b552c39 Revert pr that was mistakenly merged (#11844) 2026-02-02 15:28:02 -06:00
opencode-agent[bot] a3f1918489 chore: generate 2026-02-02 20:53:36 +00:00
Luiz Guilherme D'Abruzzo Pereira a9fca05d8b feat(server): add --mdns-domain flag to customize mDNS hostname (#11796) 2026-02-02 14:52:32 -06:00
Alex Yaroshuk 824165eb79 feat(app): add workspace toggle command to command palette and prompt input (#11810) 2026-02-02 14:36:00 -06:00
opencode-agent[bot] 562c9d76d9 chore: generate 2026-02-02 20:27:15 +00:00
Adam c002ca03ba feat(app): search through sessions 2026-02-02 14:26:09 -06:00
Adam befb5d54fb chore: cleanup 2026-02-02 14:24:25 -06:00
Adam 69f5f657f2 chore: cleanup 2026-02-02 14:24:25 -06:00
Adam 0405b425f5 feat(app): file search 2026-02-02 14:24:24 -06:00
Adam 70cf609ce9 Revert "feat(ui): Select, dropdown, popover styles & transitions (#11675)"
This reverts commit 377bf7ff21.
2026-02-02 14:24:24 -06:00
Adam 2f76b49df3 Revert "feat(ui): Smooth fading out on scroll, style fixes (#11683)"
This reverts commit e445dc0746.
2026-02-02 14:24:24 -06:00
Adam dfd5f38408 fix(app): icon sizes 2026-02-02 14:24:24 -06:00
Adam 3b93e8d95c fix(app): added/deleted file status now correctly calculated 2026-02-02 14:24:23 -06:00
Adam 23631a9393 fix(app): navigate to last project on open 2026-02-02 14:24:23 -06:00
Adam f1e0c31b8f fix(app): button heights 2026-02-02 14:24:23 -06:00
Adam 30a25e4edc fix(app): user messages not rendering consistently 2026-02-02 14:24:23 -06:00
Adam ea1aba4192 feat(app): project context menu on right-click 2026-02-02 14:24:22 -06:00
Filip b9aad20be6 fix(app): open project search (#11783) 2026-02-02 12:15:53 -06:00
Pablo P Varela 965f32ad63 fix(tui): respect terminal transparency in system theme (#8467) 2026-02-02 11:36:47 -06:00
opencode-agent[bot] cf828fff85 chore: update nix node_modules hashes 2026-02-02 16:56:31 +00:00
Vladimir Glafirov cf8b033be1 feat(provider): add User-Agent header for GitLab AI Gateway requests (#11818) 2026-02-02 10:41:02 -06:00
Goni Zahavy 1bd5dc5382 ci: add ratelimits handling for close-stale-prs.yml (#11578) 2026-02-02 10:13:48 -06:00
235 changed files with 8690 additions and 3649 deletions
+1
View File
@@ -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
+113 -19
View File
@@ -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(`=============================`)
+1
View File
@@ -5,6 +5,7 @@ node_modules
.env .env
.idea .idea
.vscode .vscode
.codex
*~ *~
playground playground
tmp tmp
+1 -7
View File
@@ -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,
+8 -8
View File
@@ -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.
--- ---
+136 -29
View File
@@ -23,7 +23,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.1.48", "version": "1.1.52",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -44,7 +44,7 @@
"@thisbeyond/solid-dnd": "0.7.5", "@thisbeyond/solid-dnd": "0.7.5",
"diff": "catalog:", "diff": "catalog:",
"fuzzysort": "catalog:", "fuzzysort": "catalog:",
"ghostty-web": "0.3.0", "ghostty-web": "0.4.0",
"luxon": "catalog:", "luxon": "catalog:",
"marked": "catalog:", "marked": "catalog:",
"marked-shiki": "catalog:", "marked-shiki": "catalog:",
@@ -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.52",
"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.52",
"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.52",
"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.52",
"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.52",
"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.52",
"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.52",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -258,14 +258,14 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.1.48", "version": "1.1.52",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
"dependencies": { "dependencies": {
"@actions/core": "1.11.1", "@actions/core": "1.11.1",
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.13.0", "@agentclientprotocol/sdk": "0.14.1",
"@ai-sdk/amazon-bedrock": "3.0.74", "@ai-sdk/amazon-bedrock": "3.0.74",
"@ai-sdk/anthropic": "2.0.58", "@ai-sdk/anthropic": "2.0.58",
"@ai-sdk/azure": "2.0.91", "@ai-sdk/azure": "2.0.91",
@@ -284,9 +284,10 @@
"@ai-sdk/provider-utils": "3.0.20", "@ai-sdk/provider-utils": "3.0.20",
"@ai-sdk/togetherai": "1.0.34", "@ai-sdk/togetherai": "1.0.34",
"@ai-sdk/vercel": "1.0.33", "@ai-sdk/vercel": "1.0.33",
"@ai-sdk/xai": "2.0.56", "@ai-sdk/xai": "2.0.51",
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@gitlab/gitlab-ai-provider": "3.3.1", "@gitlab/gitlab-ai-provider": "3.4.0",
"@gitlab/opencode-gitlab-auth": "1.3.2",
"@hono/standard-validator": "0.1.5", "@hono/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 +308,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 +364,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.1.48", "version": "1.1.52",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"zod": "catalog:", "zod": "catalog:",
@@ -382,7 +384,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.1.48", "version": "1.1.52",
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "0.90.10", "@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
@@ -393,7 +395,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.1.48", "version": "1.1.52",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -406,7 +408,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.1.48", "version": "1.1.52",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -448,7 +450,7 @@
}, },
"packages/util": { "packages/util": {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.1.48", "version": "1.1.52",
"dependencies": { "dependencies": {
"zod": "catalog:", "zod": "catalog:",
}, },
@@ -459,7 +461,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.1.48", "version": "1.1.52",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -494,9 +496,6 @@
"web-tree-sitter", "web-tree-sitter",
"tree-sitter-bash", "tree-sitter-bash",
], ],
"patchedDependencies": {
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch",
},
"overrides": { "overrides": {
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
@@ -557,7 +556,7 @@
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.13.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Z6/Fp4cXLbYdMXr5AK752JM5qG2VKb6ShM0Ql6FimBSckMmLyK54OA20UhPYoH4C37FSFwUTARuwQOwQUToYrw=="], "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.14.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.74", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-q83HE3FBb/HPIvjXsehrHOgCuGHPorSMFt6BYnzIYZy8gNnSqV1OWX4oXVsCAuYPPMtYW/KMK35hmoIFV8QKoQ=="], "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.74", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-q83HE3FBb/HPIvjXsehrHOgCuGHPorSMFt6BYnzIYZy8gNnSqV1OWX4oXVsCAuYPPMtYW/KMK35hmoIFV8QKoQ=="],
@@ -569,8 +568,16 @@
"@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yJ9kP5cEDJwo8qpITq5TQFD8YNfNtW+HbyvWwrKMbFzmiMvIZuk95HIaFXE7PCTuZsqMA05yYu+qX/vQ3rNKjA=="], "@ai-sdk/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=="],
@@ -595,7 +602,7 @@
"@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qwjm+HdwKasu7L9bDUryBMGKDMscIEzMUkjw/33uGdJpktzyNW13YaNIObOZ2HkskqDMIQJSd4Ao2BBT8fEYLw=="], "@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qwjm+HdwKasu7L9bDUryBMGKDMscIEzMUkjw/33uGdJpktzyNW13YaNIObOZ2HkskqDMIQJSd4Ao2BBT8fEYLw=="],
"@ai-sdk/xai": ["@ai-sdk/xai@2.0.56", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FGlqwWc3tAYqDHE8r8hQGQLcMiPUwgz90oU2QygUH930OWtCLapFkSu114DgVaIN/qoM1DUX+inv0Ee74Fgp5g=="], "@ai-sdk/xai": ["@ai-sdk/xai@2.0.51", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AI3le03qiegkZvn9hpnpDwez49lOvQLj4QUBT8H41SMbrdTYOxn3ktTwrsSu90cNDdzKGMvoH0u2GHju1EdnCg=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
@@ -911,8 +918,22 @@
"@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6" } }, "sha512-PBFa1wGyYzRExMDzBmAWC6/kdfG1oLn4pLpBeTfIRrALPjcGA/59HP3e7q9J0Smk4pC7U+lWkA2LHR8FYV8U7Q=="], "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6" } }, "sha512-PBFa1wGyYzRExMDzBmAWC6/kdfG1oLn4pLpBeTfIRrALPjcGA/59HP3e7q9J0Smk4pC7U+lWkA2LHR8FYV8U7Q=="],
"@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
"@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
"@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="],
"@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
"@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
"@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="],
"@fastify/rate-limit": ["@fastify/rate-limit@10.3.0", "", { "dependencies": { "@lukeed/ms": "^2.0.2", "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q=="],
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
@@ -925,7 +946,9 @@
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.3.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-J4/LfVcxOKbR2gfoBWRKp1BpWppprC2Cz/Ff5E0B/0lS341CDtZwzkgWvHfkM/XU6q83JRs059dS0cR8VOODOQ=="], "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.4.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-1fEZgqjSZ0WLesftw/J5UtFuJCYFDvCZCHhTH5PZAmpDEmCwllJBoe84L3+vIk38V2FGDMTW128iKTB2mVzr3A=="],
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.2", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-pvGrC+aDVLY8bRCC/fZaG/Qihvt2r4by5xbTo5JTSz9O7yIcR6xG2d9Wkuu4bcXFz674z2C+i5bUk+J/RSdBpg=="],
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
@@ -1119,6 +1142,8 @@
"@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="],
"@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
@@ -1359,6 +1384,8 @@
"@pierre/diffs": ["@pierre/diffs@1.0.2", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/transformers": "3.19.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.19.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-RkFSDD5X/U+8QjyilPViYGJfmJNWXR17zTL8zw48+DcVC1Ujbh6I1edyuRnFfgRzpft05x2DSCkz2cjoIAxPvQ=="], "@pierre/diffs": ["@pierre/diffs@1.0.2", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/transformers": "3.19.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.19.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-RkFSDD5X/U+8QjyilPViYGJfmJNWXR17zTL8zw48+DcVC1Ujbh6I1edyuRnFfgRzpft05x2DSCkz2cjoIAxPvQ=="],
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@planetscale/database": ["@planetscale/database@1.19.0", "", {}, "sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA=="], "@planetscale/database": ["@planetscale/database@1.19.0", "", {}, "sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA=="],
@@ -1935,6 +1962,8 @@
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
@@ -1949,6 +1978,8 @@
"ai": ["ai@5.0.124", "", { "dependencies": { "@ai-sdk/gateway": "2.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Li6Jw9F9qsvFJXZPBfxj38ddP2iURCnMs96f9Q3OeQzrDVcl1hvtwSEAuxA/qmfh6SDV2ERqFUOFzigvr0697g=="], "ai": ["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=="],
@@ -2009,10 +2040,14 @@
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
"autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="], "autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"avvio": ["avvio@9.1.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw=="],
"await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
"aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="], "aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="],
@@ -2097,7 +2132,7 @@
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="], "bun-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=="],
@@ -2455,16 +2490,26 @@
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-json-stringify": ["fast-json-stringify@6.2.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-Eaf/KNIDwHkzfyeQFNfLXJnQ7cl1XQI3+zRqmPlvtkMigbXnAcasTrvJQmquBSxKfFGeRA6PFog8t+hFmpDoWw=="],
"fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="], "fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="],
"fastify": ["fastify@5.7.4", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA=="],
"fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="],
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
@@ -2479,6 +2524,8 @@
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
"find-my-way": ["find-my-way@9.4.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w=="],
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"finity": ["finity@0.5.4", "", {}, "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA=="], "finity": ["finity@0.5.4", "", {}, "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA=="],
@@ -2555,7 +2602,7 @@
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
"ghostty-web": ["ghostty-web@0.3.0", "", {}, "sha512-SAdSHWYF20GMZUB0n8kh1N6Z4ljMnuUqT8iTB2n5FAPswEV10MejEpLlhW/769GL5+BQa1NYwEg9y/XCckV5+A=="], "ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="],
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
@@ -2845,6 +2892,8 @@
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
@@ -2881,6 +2930,8 @@
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
"light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="],
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
@@ -3183,6 +3234,8 @@
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="], "omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
@@ -3289,6 +3342,12 @@
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
"pino": ["pino@10.3.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA=="],
"pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="], "pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="],
@@ -3341,6 +3400,8 @@
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
"promise.allsettled": ["promise.allsettled@1.0.7", "", { "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" } }, "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA=="], "promise.allsettled": ["promise.allsettled@1.0.7", "", { "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" } }, "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
@@ -3363,6 +3424,8 @@
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
@@ -3397,6 +3460,8 @@
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
"recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
@@ -3463,6 +3528,8 @@
"restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="],
"ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="],
"retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="],
"retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="],
@@ -3475,6 +3542,8 @@
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
"rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="], "rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="],
@@ -3497,6 +3566,10 @@
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"safe-regex2": ["safe-regex2@5.0.0", "", { "dependencies": { "ret": "~0.5.0" } }, "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="], "sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
@@ -3505,6 +3578,8 @@
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
"secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
"selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
@@ -3519,6 +3594,8 @@
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
@@ -3581,6 +3658,8 @@
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="], "solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -3589,6 +3668,8 @@
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
@@ -3691,6 +3772,8 @@
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
"thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="],
"three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="], "three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="],
"thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="],
@@ -3989,6 +4072,8 @@
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], "@ai-sdk/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=="],
@@ -4003,7 +4088,7 @@
"@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], "@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], "@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
@@ -4071,6 +4156,8 @@
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
"@gitlab/gitlab-ai-provider/openai": ["openai@6.17.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-NHRpPEUPzAvFOAFs9+9pC6+HCw/iWsYsKCMPXH5Kw7BpMxqd8g/A07/1o7Gx2TWtCnzevVRyKMRFqyiHyAlqcA=="], "@gitlab/gitlab-ai-provider/openai": ["openai@6.17.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-NHRpPEUPzAvFOAFs9+9pC6+HCw/iWsYsKCMPXH5Kw7BpMxqd8g/A07/1o7Gx2TWtCnzevVRyKMRFqyiHyAlqcA=="],
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
@@ -4273,6 +4360,16 @@
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="],
"ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="],
"ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
"ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
"ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@2.0.56", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FGlqwWc3tAYqDHE8r8hQGQLcMiPUwgz90oU2QygUH930OWtCLapFkSu114DgVaIN/qoM1DUX+inv0Ee74Fgp5g=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "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=="],
@@ -4361,6 +4458,8 @@
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="], "md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
@@ -4805,6 +4904,14 @@
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "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
View File
@@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-4I0lpBnbAi7IZMURTMLysjrqdsNvXJf8802NrJnpdks=", "x86_64-linux": "sha256-ufEpxjmlJeft9tI+WxxO+Zbh1pdAaLOURCDBpoQqR0w=",
"aarch64-linux": "sha256-WOGKsPlcQVSbL8TDr1JYO/2ucPTV2Hy0TXJKWv8EoVw=", "aarch64-linux": "sha256-z3K6W5oYZNUdV0rjoAZjvNQcifM5bXamLIrD+ZvJ4kA=",
"aarch64-darwin": "sha256-LuvjwGm1QsHoLxuvSSp4VsDIv02Z/rTONsU32arQMuw=", "aarch64-darwin": "sha256-+QikplmNhxGF2Nd4L1BG/xyl+24GVhDYMTtK6xCKy/s=",
"x86_64-darwin": "sha256-AbglfgCWj/r+wHfle+e+D3b/xPcwwg4IK7j5iwn9nzw=" "x86_64-darwin": "sha256-hAcrCT2X02ymwgj/0BAmD2gF66ylGYzbfcqPta/LVEU="
} }
} }
+3 -3
View File
@@ -7,6 +7,8 @@
"packageManager": "bun@1.3.5", "packageManager": "bun@1.3.5",
"scripts": { "scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev:desktop": "bun --cwd packages/desktop tauri dev",
"dev:web": "bun --cwd packages/app dev",
"typecheck": "bun turbo typecheck", "typecheck": "bun turbo typecheck",
"prepare": "husky", "prepare": "husky",
"random": "echo 'Random script'", "random": "echo 'Random script'",
@@ -98,7 +100,5 @@
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@types/node": "catalog:" "@types/node": "catalog:"
}, },
"patchedDependencies": { "patchedDependencies": {}
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
}
} }
+64 -6
View File
@@ -21,7 +21,12 @@ import {
import type { createSdk } from "./utils" import type { createSdk } from "./utils"
export async function defocus(page: Page) { export async function defocus(page: Page) {
await page.mouse.click(5, 5) await page
.evaluate(() => {
const el = document.activeElement
if (el instanceof HTMLElement) el.blur()
})
.catch(() => undefined)
} }
export async function openPalette(page: Page) { export async function openPalette(page: Page) {
@@ -68,14 +73,50 @@ export async function toggleSidebar(page: Page) {
export async function openSidebar(page: Page) { export async function openSidebar(page: Page) {
if (!(await isSidebarClosed(page))) return if (!(await isSidebarClosed(page))) return
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
const visible = await button
.isVisible()
.then((x) => x)
.catch(() => false)
if (visible) await button.click()
if (!visible) await toggleSidebar(page)
const main = page.locator("main")
const opened = await expect(main)
.not.toHaveClass(/xl:border-l/, { timeout: 1500 })
.then(() => true)
.catch(() => false)
if (opened) return
await toggleSidebar(page) await toggleSidebar(page)
await expect(page.locator("main")).not.toHaveClass(/xl:border-l/) await expect(main).not.toHaveClass(/xl:border-l/)
} }
export async function closeSidebar(page: Page) { export async function closeSidebar(page: Page) {
if (await isSidebarClosed(page)) return if (await isSidebarClosed(page)) return
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
const visible = await button
.isVisible()
.then((x) => x)
.catch(() => false)
if (visible) await button.click()
if (!visible) await toggleSidebar(page)
const main = page.locator("main")
const closed = await expect(main)
.toHaveClass(/xl:border-l/, { timeout: 1500 })
.then(() => true)
.catch(() => false)
if (closed) return
await toggleSidebar(page) await toggleSidebar(page)
await expect(page.locator("main")).toHaveClass(/xl:border-l/) await expect(main).toHaveClass(/xl:border-l/)
} }
export async function openSettings(page: Page) { export async function openSettings(page: Page) {
@@ -182,13 +223,30 @@ export async function hoverSessionItem(page: Page, sessionID: string) {
} }
export async function openSessionMoreMenu(page: Page, sessionID: string) { export async function openSessionMoreMenu(page: Page, sessionID: string) {
const sessionEl = await hoverSessionItem(page, sessionID) await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`))
const menuTrigger = sessionEl.locator(dropdownMenuTriggerSelector).first() const scroller = page.locator(".session-scroller").first()
await expect(scroller).toBeVisible()
await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
const menu = page
.locator(dropdownMenuContentSelector)
.filter({ has: page.getByRole("menuitem", { name: /rename/i }) })
.filter({ has: page.getByRole("menuitem", { name: /archive/i }) })
.filter({ has: page.getByRole("menuitem", { name: /delete/i }) })
.first()
const opened = await menu
.isVisible()
.then((x) => x)
.catch(() => false)
if (opened) return menu
const menuTrigger = scroller.getByRole("button", { name: /more options/i }).first()
await expect(menuTrigger).toBeVisible() await expect(menuTrigger).toBeVisible()
await menuTrigger.click() await menuTrigger.click()
const menu = page.locator(dropdownMenuContentSelector).first()
await expect(menu).toBeVisible() await expect(menu).toBeVisible()
return menu return menu
} }
+44 -13
View File
@@ -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 }
+44 -43
View File
@@ -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)
} }
+114 -172
View File
@@ -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)
}
}) })
+56 -14
View File
@@ -11,57 +11,98 @@ import { sessionItemSelector, inlineInputSelector } from "../selectors"
const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1" const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1"
test("sidebar session can be renamed", async ({ page, sdk, gotoSession }) => { type Sdk = Parameters<typeof withSession>[0]
async function seedMessage(sdk: Sdk, sessionID: string) {
await sdk.session.promptAsync({
sessionID,
noReply: true,
parts: [{ type: "text", text: "e2e seed" }],
})
await expect
.poll(
async () => {
const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? [])
return messages.length
},
{ timeout: 30_000 },
)
.toBeGreaterThan(0)
}
test("session can be renamed via header menu", async ({ page, sdk, gotoSession }) => {
const stamp = Date.now() const stamp = Date.now()
const originalTitle = `e2e rename test ${stamp}` const originalTitle = `e2e rename test ${stamp}`
const newTitle = `e2e renamed ${stamp}` const newTitle = `e2e renamed ${stamp}`
await withSession(sdk, originalTitle, async (session) => { await withSession(sdk, originalTitle, async (session) => {
await seedMessage(sdk, session.id)
await gotoSession(session.id) await gotoSession(session.id)
await openSidebar(page)
const menu = await openSessionMoreMenu(page, session.id) const menu = await openSessionMoreMenu(page, session.id)
await clickMenuItem(menu, /rename/i) await clickMenuItem(menu, /rename/i)
const input = page.locator(sessionItemSelector(session.id)).locator(inlineInputSelector).first() const input = page.locator(".session-scroller").locator(inlineInputSelector).first()
await expect(input).toBeVisible() await expect(input).toBeVisible()
await input.fill(newTitle) await input.fill(newTitle)
await input.press("Enter") await input.press("Enter")
await expect(page.locator(sessionItemSelector(session.id)).locator("a").first()).toContainText(newTitle) await expect(page.getByRole("heading", { level: 1 }).first()).toContainText(newTitle)
}) })
}) })
test("sidebar session can be archived", async ({ page, sdk, gotoSession }) => { test("session can be archived via header menu", async ({ page, sdk, gotoSession }) => {
const stamp = Date.now() const stamp = Date.now()
const title = `e2e archive test ${stamp}` const title = `e2e archive test ${stamp}`
await withSession(sdk, title, async (session) => { await withSession(sdk, title, async (session) => {
await seedMessage(sdk, session.id)
await gotoSession(session.id) await gotoSession(session.id)
await openSidebar(page)
const sessionEl = page.locator(sessionItemSelector(session.id))
const menu = await openSessionMoreMenu(page, session.id) const menu = await openSessionMoreMenu(page, session.id)
await clickMenuItem(menu, /archive/i) await clickMenuItem(menu, /archive/i)
await expect(sessionEl).not.toBeVisible() await expect
.poll(
async () => {
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
return data?.time?.archived
},
{ timeout: 30_000 },
)
.not.toBeUndefined()
await openSidebar(page)
await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
}) })
}) })
test("sidebar session can be deleted", async ({ page, sdk, gotoSession }) => { test("session can be deleted via header menu", async ({ page, sdk, gotoSession }) => {
const stamp = Date.now() const stamp = Date.now()
const title = `e2e delete test ${stamp}` const title = `e2e delete test ${stamp}`
await withSession(sdk, title, async (session) => { await withSession(sdk, title, async (session) => {
await seedMessage(sdk, session.id)
await gotoSession(session.id) await gotoSession(session.id)
await openSidebar(page)
const sessionEl = page.locator(sessionItemSelector(session.id))
const menu = await openSessionMoreMenu(page, session.id) const menu = await openSessionMoreMenu(page, session.id)
await clickMenuItem(menu, /delete/i) await clickMenuItem(menu, /delete/i)
await confirmDialog(page, /delete/i) await confirmDialog(page, /delete/i)
await expect(sessionEl).not.toBeVisible() await expect
.poll(
async () => {
const data = await sdk.session
.get({ sessionID: session.id })
.then((r) => r.data)
.catch(() => undefined)
return data?.id
},
{ timeout: 30_000 },
)
.toBeUndefined()
await openSidebar(page)
await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
}) })
}) })
@@ -72,6 +113,7 @@ test("session can be shared and unshared via header button", async ({ page, sdk,
const title = `e2e share test ${stamp}` const title = `e2e share test ${stamp}`
await withSession(sdk, title, async (session) => { await withSession(sdk, title, async (session) => {
await seedMessage(sdk, session.id)
await gotoSession(session.id) await gotoSession(session.id)
const { rightSection, popoverBody } = await openSharePopover(page) const { rightSection, popoverBody } = await openSharePopover(page)
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures" import { test, expect } from "../fixtures"
import { openStatusPopover, defocus } from "../actions" import { openStatusPopover } from "../actions"
test("status popover opens and shows tabs", async ({ page, gotoSession }) => { test("status popover opens and shows tabs", async ({ page, gotoSession }) => {
await gotoSession() await gotoSession()
@@ -88,7 +88,7 @@ test("status popover closes when clicking outside", async ({ page, gotoSession }
const { popoverBody } = await openStatusPopover(page) const { popoverBody } = await openStatusPopover(page)
await expect(popoverBody).toBeVisible() await expect(popoverBody).toBeVisible()
await defocus(page) await page.getByRole("main").click({ position: { x: 5, y: 5 } })
await expect(popoverBody).toHaveCount(0) await expect(popoverBody).toHaveCount(0)
}) })
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.1.48", "version": "1.1.52",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -54,7 +54,7 @@
"@thisbeyond/solid-dnd": "0.7.5", "@thisbeyond/solid-dnd": "0.7.5",
"diff": "catalog:", "diff": "catalog:",
"fuzzysort": "catalog:", "fuzzysort": "catalog:",
"ghostty-web": "0.3.0", "ghostty-web": "0.4.0",
"luxon": "catalog:", "luxon": "catalog:",
"marked": "catalog:", "marked": "catalog:",
"marked-shiki": "catalog:", "marked-shiki": "catalog:",
+1 -3
View File
@@ -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"]],
+28 -16
View File
@@ -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
} }
@@ -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,9 +41,13 @@ 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))
const view = createMemo(() => layout.view(sessionKey))
const state = { cleanup: undefined as (() => void) | void, committed: false } const state = { cleanup: undefined as (() => void) | void, committed: false }
const [grouped, setGrouped] = createSignal(false) const [grouped, setGrouped] = createSignal(false)
const common = [ const common = [
@@ -73,6 +85,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 +180,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 +266,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) => {
@@ -162,6 +283,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
const value = file.tab(path) const value = file.tab(path)
tabs().open(value) tabs().open(value)
file.load(path) file.load(path)
if (!view().reviewPanel.opened()) view().reviewPanel.open()
layout.fileTree.open() layout.fileTree.open()
layout.fileTree.setTab("all") layout.fileTree.setTab("all")
props.onOpenFile?.(path) props.onOpenFile?.(path)
@@ -178,6 +300,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 +330,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 +350,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"
+51 -5
View File
@@ -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 = (
@@ -194,7 +241,7 @@ export default function FileTree(props: {
: kind === "del" : kind === "del"
? "color: var(--icon-diff-delete-base)" ? "color: var(--icon-diff-delete-base)"
: kind === "mix" : kind === "mix"
? "color: var(--icon-diff-modified-base)" ? "color: var(--icon-warning-active)"
: undefined : undefined
return ( return (
<span <span
@@ -221,7 +268,7 @@ export default function FileTree(props: {
? "color: var(--icon-diff-add-base)" ? "color: var(--icon-diff-add-base)"
: kind === "del" : kind === "del"
? "color: var(--icon-diff-delete-base)" ? "color: var(--icon-diff-delete-base)"
: "color: var(--icon-diff-modified-base)" : "color: var(--icon-warning-active)"
return ( return (
<span class="shrink-0 w-4 text-center text-12-medium" style={color}> <span class="shrink-0 w-4 text-center text-12-medium" style={color}>
@@ -236,7 +283,7 @@ export default function FileTree(props: {
? "background-color: var(--icon-diff-add-base)" ? "background-color: var(--icon-diff-add-base)"
: kind === "del" : kind === "del"
? "background-color: var(--icon-diff-delete-base)" ? "background-color: var(--icon-diff-delete-base)"
: "background-color: var(--icon-diff-modified-base)" : "background-color: var(--icon-warning-active)"
return <div class="shrink-0 size-1.5 mr-1.5 rounded-full" style={color} /> return <div class="shrink-0 size-1.5 mr-1.5 rounded-full" style={color} />
} }
@@ -274,7 +321,6 @@ export default function FileTree(props: {
return ( return (
<Tooltip <Tooltip
forceMount={false}
openDelay={2000} openDelay={2000}
placement="bottom-start" placement="bottom-start"
class="w-full" class="w-full"
+71 -88
View File
@@ -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"
@@ -175,6 +172,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
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))
const view = createMemo(() => layout.view(sessionKey))
const commentInReview = (path: string) => { const commentInReview = (path: string) => {
const sessionID = params.id const sessionID = params.id
@@ -193,12 +191,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path)) const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path))
if (wantsReview) { if (wantsReview) {
if (!view().reviewPanel.opened()) view().reviewPanel.open()
layout.fileTree.open() layout.fileTree.open()
layout.fileTree.setTab("changes") layout.fileTree.setTab("changes")
requestAnimationFrame(() => comments.setFocus(focus)) requestAnimationFrame(() => comments.setFocus(focus))
return return
} }
if (!view().reviewPanel.opened()) view().reviewPanel.open()
layout.fileTree.open() layout.fileTree.open()
layout.fileTree.setTab("all") layout.fileTree.setTab("all")
const tab = files.tab(item.path) const tab = files.tab(item.path)
@@ -1135,7 +1135,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
} }
@@ -1223,7 +1223,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}) })
return undefined return undefined
}) })
if (session) navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`) if (session) {
layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
}
} }
if (!session) return if (!session) return
@@ -1257,7 +1260,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 +1283,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 +1439,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 +1456,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 +1466,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 +1474,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 +1484,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 +1493,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 +1506,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 +1527,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 +1552,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 +1580,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 +1590,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 +1600,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 +1624,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}) })
} }
const currrentModelVariant = createMemo(() => {
const modelVariant = local.model.variant.current() ?? ""
return modelVariant === "xhigh"
? "xHigh"
: modelVariant.length > 0
? modelVariant[0].toUpperCase() + modelVariant.slice(1)
: "Default"
})
const reasoningPercentage = createMemo(() => {
const variants = local.model.variant.list()
const current = local.model.variant.current()
const totalEntries = variants.length + 1
if (totalEntries <= 2 || current === "Default") {
return 0
}
const currentIndex = current ? variants.indexOf(current) + 1 : 0
return ((currentIndex + 1) / totalEntries) * 100
}, [local.model.variant])
return ( 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 +1676,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</> </>
} }
> >
<Icon name="brain" size="normal" class="text-icon-info-active shrink-0" /> <Icon name="brain" size="small" class="text-icon-info-active shrink-0" />
<span class="text-14-regular text-text-strong whitespace-nowrap"> <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 +1741,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 +1782,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 +1799,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 +1829,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 +1902,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 +1915,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 +1923,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 +1933,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 +2002,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 +2024,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 +2036,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 +2066,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 +2074,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>
@@ -0,0 +1,295 @@
import { For, Show, createMemo, type Component } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { showToast } from "@opencode-ai/ui/toast"
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
import { useLanguage } from "@/context/language"
import { useSDK } from "@/context/sdk"
export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => {
const sdk = useSDK()
const language = useLanguage()
const questions = createMemo(() => props.request.questions)
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
const [store, setStore] = createStore({
tab: 0,
answers: [] as QuestionAnswer[],
custom: [] as string[],
editing: false,
sending: false,
})
const question = createMemo(() => questions()[store.tab])
const confirm = createMemo(() => !single() && store.tab === questions().length)
const options = createMemo(() => question()?.options ?? [])
const input = createMemo(() => store.custom[store.tab] ?? "")
const multi = createMemo(() => question()?.multiple === true)
const customPicked = createMemo(() => {
const value = input()
if (!value) return false
return store.answers[store.tab]?.includes(value) ?? false
})
const fail = (err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
}
const reply = (answers: QuestionAnswer[]) => {
if (store.sending) return
setStore("sending", true)
sdk.client.question
.reply({ requestID: props.request.id, answers })
.catch(fail)
.finally(() => setStore("sending", false))
}
const reject = () => {
if (store.sending) return
setStore("sending", true)
sdk.client.question
.reject({ requestID: props.request.id })
.catch(fail)
.finally(() => setStore("sending", false))
}
const submit = () => {
reply(questions().map((_, i) => store.answers[i] ?? []))
}
const pick = (answer: string, custom: boolean = false) => {
const answers = [...store.answers]
answers[store.tab] = [answer]
setStore("answers", answers)
if (custom) {
const inputs = [...store.custom]
inputs[store.tab] = answer
setStore("custom", inputs)
}
if (single()) {
reply([[answer]])
return
}
setStore("tab", store.tab + 1)
}
const toggle = (answer: string) => {
const existing = store.answers[store.tab] ?? []
const next = [...existing]
const index = next.indexOf(answer)
if (index === -1) next.push(answer)
if (index !== -1) next.splice(index, 1)
const answers = [...store.answers]
answers[store.tab] = next
setStore("answers", answers)
}
const selectTab = (index: number) => {
setStore("tab", index)
setStore("editing", false)
}
const selectOption = (optIndex: number) => {
if (store.sending) return
if (optIndex === options().length) {
setStore("editing", true)
return
}
const opt = options()[optIndex]
if (!opt) return
if (multi()) {
toggle(opt.label)
return
}
pick(opt.label)
}
const handleCustomSubmit = (e: Event) => {
e.preventDefault()
if (store.sending) return
const value = input().trim()
if (!value) {
setStore("editing", false)
return
}
if (multi()) {
const existing = store.answers[store.tab] ?? []
const next = [...existing]
if (!next.includes(value)) next.push(value)
const answers = [...store.answers]
answers[store.tab] = next
setStore("answers", answers)
setStore("editing", false)
return
}
pick(value, true)
setStore("editing", false)
}
return (
<div data-component="question-prompt">
<Show when={!single()}>
<div data-slot="question-tabs">
<For each={questions()}>
{(q, index) => {
const active = () => index() === store.tab
const answered = () => (store.answers[index()]?.length ?? 0) > 0
return (
<button
data-slot="question-tab"
data-active={active()}
data-answered={answered()}
disabled={store.sending}
onClick={() => selectTab(index())}
>
{q.header}
</button>
)
}}
</For>
<button
data-slot="question-tab"
data-active={confirm()}
disabled={store.sending}
onClick={() => selectTab(questions().length)}
>
{language.t("ui.common.confirm")}
</button>
</div>
</Show>
<Show when={!confirm()}>
<div data-slot="question-content">
<div data-slot="question-text">
{question()?.question}
{multi() ? " " + language.t("ui.question.multiHint") : ""}
</div>
<div data-slot="question-options">
<For each={options()}>
{(opt, i) => {
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
return (
<button
data-slot="question-option"
data-picked={picked()}
disabled={store.sending}
onClick={() => selectOption(i())}
>
<span data-slot="option-label">{opt.label}</span>
<Show when={opt.description}>
<span data-slot="option-description">{opt.description}</span>
</Show>
<Show when={picked()}>
<Icon name="check-small" size="normal" />
</Show>
</button>
)
}}
</For>
<button
data-slot="question-option"
data-picked={customPicked()}
disabled={store.sending}
onClick={() => selectOption(options().length)}
>
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
<Show when={!store.editing && input()}>
<span data-slot="option-description">{input()}</span>
</Show>
<Show when={customPicked()}>
<Icon name="check-small" size="normal" />
</Show>
</button>
<Show when={store.editing}>
<form data-slot="custom-input-form" onSubmit={handleCustomSubmit}>
<input
ref={(el) => setTimeout(() => el.focus(), 0)}
type="text"
data-slot="custom-input"
placeholder={language.t("ui.question.custom.placeholder")}
value={input()}
disabled={store.sending}
onInput={(e) => {
const inputs = [...store.custom]
inputs[store.tab] = e.currentTarget.value
setStore("custom", inputs)
}}
/>
<Button type="submit" variant="primary" size="small" disabled={store.sending}>
{multi() ? language.t("ui.common.add") : language.t("ui.common.submit")}
</Button>
<Button
type="button"
variant="ghost"
size="small"
disabled={store.sending}
onClick={() => setStore("editing", false)}
>
{language.t("ui.common.cancel")}
</Button>
</form>
</Show>
</div>
</div>
</Show>
<Show when={confirm()}>
<div data-slot="question-review">
<div data-slot="review-title">{language.t("ui.messagePart.review.title")}</div>
<For each={questions()}>
{(q, index) => {
const value = () => store.answers[index()]?.join(", ") ?? ""
const answered = () => Boolean(value())
return (
<div data-slot="review-item">
<span data-slot="review-label">{q.question}</span>
<span data-slot="review-value" data-answered={answered()}>
{answered() ? value() : language.t("ui.question.review.notAnswered")}
</span>
</div>
)
}}
</For>
</div>
</Show>
<div data-slot="question-actions">
<Button variant="ghost" size="small" onClick={reject} disabled={store.sending}>
{language.t("ui.common.dismiss")}
</Button>
<Show when={!single()}>
<Show when={confirm()}>
<Button variant="primary" size="small" onClick={submit} disabled={store.sending}>
{language.t("ui.common.submit")}
</Button>
</Show>
<Show when={!confirm() && multi()}>
<Button
variant="secondary"
size="small"
onClick={() => selectTab(store.tab + 1)}
disabled={store.sending || (store.answers[store.tab]?.length ?? 0) === 0}
>
{language.t("ui.common.next")}
</Button>
</Show>
</Show>
</div>
</div>
)
}
@@ -23,6 +23,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const variant = createMemo(() => props.variant ?? "button") const variant = createMemo(() => props.variant ?? "button")
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))
const view = createMemo(() => layout.view(sessionKey))
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
const usd = createMemo( const usd = createMemo(
@@ -57,6 +58,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const openContext = () => { const openContext = () => {
if (!params.id) return if (!params.id) return
if (!view().reviewPanel.opened()) view().reviewPanel.open()
layout.fileTree.open() layout.fileTree.open()
layout.fileTree.setTab("all") layout.fileTree.setTab("all")
tabs().open("context") tabs().open("context")
@@ -64,7 +66,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>
) )
@@ -6,18 +6,23 @@ import { useLayout } from "@/context/layout"
import { useCommand } from "@/context/command" import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useServer } from "@/context/server"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSDK } from "@/context/global-sdk"
import { getFilename } from "@opencode-ai/util/path" import { getFilename } from "@opencode-ai/util/path"
import { decode64 } from "@/utils/base64" import { decode64 } from "@/utils/base64"
import { Persist, persisted } from "@/utils/persist"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { AppIcon } from "@opencode-ai/ui/app-icon"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { Popover } from "@opencode-ai/ui/popover" 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 { showToast } from "@opencode-ai/ui/toast"
import { StatusPopover } from "../status-popover" import { StatusPopover } from "../status-popover"
export function SessionHeader() { export function SessionHeader() {
@@ -25,6 +30,7 @@ export function SessionHeader() {
const layout = useLayout() const layout = useLayout()
const params = useParams() const params = useParams()
const command = useCommand() const command = useCommand()
const server = useServer()
const sync = useSync() const sync = useSync()
const platform = usePlatform() const platform = usePlatform()
const language = useLanguage() const language = useLanguage()
@@ -48,6 +54,117 @@ export function SessionHeader() {
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const view = createMemo(() => layout.view(sessionKey)) const view = createMemo(() => layout.view(sessionKey))
const OPEN_APPS = [
"vscode",
"cursor",
"zed",
"textmate",
"antigravity",
"finder",
"terminal",
"iterm2",
"ghostty",
"xcode",
"android-studio",
"powershell",
] as const
type OpenApp = (typeof OPEN_APPS)[number]
const os = createMemo<"macos" | "windows" | "linux" | "unknown">(() => {
if (platform.platform === "desktop" && platform.os) return platform.os
if (typeof navigator !== "object") return "unknown"
const value = navigator.platform || navigator.userAgent
if (/Mac/i.test(value)) return "macos"
if (/Win/i.test(value)) return "windows"
if (/Linux/i.test(value)) return "linux"
return "unknown"
})
const options = createMemo(() => {
if (os() === "macos") {
return [
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "Visual Studio Code" },
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" },
{ id: "zed", label: "Zed", icon: "zed", openWith: "Zed" },
{ id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" },
{ id: "antigravity", label: "Antigravity", icon: "antigravity", openWith: "Antigravity" },
{ id: "finder", label: "Finder", icon: "finder" },
{ id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" },
{ id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" },
{ id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" },
{ id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" },
{ id: "android-studio", label: "Android Studio", icon: "android-studio", openWith: "Android Studio" },
] as const
}
if (os() === "windows") {
return [
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
{ id: "finder", label: "File Explorer", icon: "finder" },
{ id: "powershell", label: "PowerShell", icon: "powershell", openWith: "powershell" },
] as const
}
return [
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
{ id: "finder", label: "File Manager", icon: "finder" },
] as const
})
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
createEffect(() => {
if (platform.platform !== "desktop") return
const value = prefs.app
if (options().some((o) => o.id === value)) return
setPrefs("app", options()[0]?.id ?? "finder")
})
const openDir = (app: OpenApp) => {
const directory = projectDirectory()
if (!directory) return
if (!canOpen()) return
const item = options().find((o) => o.id === app)
const openWith = item && "openWith" in item ? item.openWith : undefined
Promise.resolve(platform.openPath?.(directory, openWith)).catch((err: unknown) => {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
})
}
const copyPath = () => {
const directory = projectDirectory()
if (!directory) return
navigator.clipboard
.writeText(directory)
.then(() => {
showToast({
variant: "success",
icon: "circle-check",
title: language.t("session.share.copy.copied"),
description: directory,
})
})
.catch((err: unknown) => {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
})
}
const [state, setState] = createStore({ const [state, setState] = createStore({
share: false, share: false,
unshare: false, unshare: false,
@@ -150,6 +267,76 @@ 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">
<Show when={projectDirectory()}>
<Show
when={canOpen()}
fallback={
<Button
variant="ghost"
class="rounded-sm h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none"
onClick={copyPath}
aria-label={language.t("session.header.open.copyPath")}
>
<Icon name="copy" size="small" class="text-icon-base" />
<span class="text-12-regular text-text-strong">{language.t("session.header.open.copyPath")}</span>
</Button>
}
>
<div class="flex items-center">
<Button
variant="ghost"
class="rounded-sm h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none rounded-r-none"
onClick={() => openDir(current().id)}
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
>
<AppIcon id={current().icon} class="size-5" />
<span class="text-12-regular text-text-strong">
{language.t("session.header.open.action", { app: current().label })}
</span>
</Button>
<DropdownMenu>
<DropdownMenu.Trigger
as={IconButton}
icon="chevron-down"
variant="ghost"
class="rounded-sm h-[24px] w-auto px-1.5 border-none shadow-none rounded-l-none data-[expanded]:bg-surface-raised-base-active"
aria-label={language.t("session.header.open.menu")}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content placement="bottom-end" gutter={6}>
<DropdownMenu.Group>
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
<DropdownMenu.RadioGroup
value={prefs.app}
onChange={(value) => {
if (!OPEN_APPS.includes(value as OpenApp)) return
setPrefs("app", value as OpenApp)
}}
>
{options().map((o) => (
<DropdownMenu.RadioItem value={o.id} onSelect={() => openDir(o.id)}>
<AppIcon id={o.icon} class="size-5" />
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
<DropdownMenu.ItemIndicator>
<Icon name="check-small" size="small" class="text-icon-weak" />
</DropdownMenu.ItemIndicator>
</DropdownMenu.RadioItem>
))}
</DropdownMenu.RadioGroup>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onSelect={copyPath}>
<Icon name="copy" size="small" class="text-icon-weak" />
<DropdownMenu.ItemLabel>
{language.t("session.header.open.copyPath")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
</Show>
</Show>
<StatusPopover /> <StatusPopover />
<Show when={showShare()}> <Show when={showShare()}>
<div class="flex items-center"> <div class="flex items-center">
@@ -283,27 +470,57 @@ export function SessionHeader() {
<TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}> <TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}>
<Button <Button
variant="ghost" variant="ghost"
class="group/file-tree-toggle size-6 p-0" class="group/review-toggle size-6 p-0"
onClick={() => layout.fileTree.toggle()} onClick={() => view().reviewPanel.toggle()}
aria-label={language.t("command.review.toggle")} aria-label={language.t("command.review.toggle")}
aria-expanded={layout.fileTree.opened()} aria-expanded={view().reviewPanel.opened()}
aria-controls="review-panel" aria-controls="review-panel"
> >
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0"> <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon <Icon
size="small" size="small"
name={layout.fileTree.opened() ? "layout-right-full" : "layout-right"} name={view().reviewPanel.opened() ? "layout-right-full" : "layout-right"}
class="group-hover/file-tree-toggle:hidden" class="group-hover/review-toggle:hidden"
/> />
<Icon <Icon
size="small" size="small"
name="layout-right-partial" name="layout-right-partial"
class="hidden group-hover/file-tree-toggle:inline-block" class="hidden group-hover/review-toggle:inline-block"
/> />
<Icon <Icon
size="small" size="small"
name={layout.fileTree.opened() ? "layout-right" : "layout-right-full"} name={view().reviewPanel.opened() ? "layout-right" : "layout-right-full"}
class="hidden group-active/file-tree-toggle:inline-block" class="hidden group-active/review-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
</div>
<div class="hidden md:block shrink-0">
<TooltipKeybind
title={language.t("command.fileTree.toggle")}
keybind={command.keybind("fileTree.toggle")}
>
<Button
variant="ghost"
class="group/file-tree-toggle size-6 p-0"
onClick={() => {
const opening = !layout.fileTree.opened()
if (opening && !view().reviewPanel.opened()) view().reviewPanel.open()
layout.fileTree.toggle()
}}
aria-label={language.t("command.fileTree.toggle")}
aria-expanded={layout.fileTree.opened()}
aria-controls="file-tree-panel"
>
<div class="relative flex items-center justify-center size-4">
<Icon
size="small"
name="bullet-list"
classList={{
"text-icon-strong": layout.fileTree.opened(),
"text-icon-weak": !layout.fileTree.opened(),
}}
/> />
</div> </div>
</Button> </Button>
@@ -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>
) )
} }
+61 -8
View File
@@ -1,5 +1,6 @@
import type { Ghostty, Terminal as Term, FitAddon } from "ghostty-web" import type { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js" import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
import { usePlatform } from "@/context/platform"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { monoFontFamily, useSettings } from "@/context/settings" import { monoFontFamily, useSettings } from "@/context/settings"
import { SerializeAddon } from "@/addons/serialize" import { SerializeAddon } from "@/addons/serialize"
@@ -52,6 +53,7 @@ const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
} }
export const Terminal = (props: TerminalProps) => { export const Terminal = (props: TerminalProps) => {
const platform = usePlatform()
const sdk = useSDK() const sdk = useSDK()
const settings = useSettings() const settings = useSettings()
const theme = useTheme() const theme = useTheme()
@@ -68,6 +70,7 @@ export const Terminal = (props: TerminalProps) => {
let handleTextareaBlur: () => void let handleTextareaBlur: () => void
let disposed = false let disposed = false
const cleanups: VoidFunction[] = [] const cleanups: VoidFunction[] = []
let tail = local.pty.tail ?? ""
const cleanup = () => { const cleanup = () => {
if (!cleanups.length) return if (!cleanups.length) return
@@ -135,6 +138,22 @@ export const Terminal = (props: TerminalProps) => {
focusTerminal() focusTerminal()
} }
const handleLinkClick = (event: MouseEvent) => {
if (!event.shiftKey && !event.ctrlKey && !event.metaKey) return
if (event.altKey) return
if (event.button !== 0) return
const t = term
if (!t) return
const link = (t as unknown as { currentHoveredLink?: { text: string } }).currentHoveredLink
if (!link?.text) return
event.preventDefault()
event.stopImmediatePropagation()
platform.openLink(link.text)
}
onMount(() => { onMount(() => {
const run = async () => { const run = async () => {
const loaded = await loadGhostty() const loaded = await loadGhostty()
@@ -146,6 +165,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
@@ -166,6 +186,7 @@ export const Terminal = (props: TerminalProps) => {
fontSize: 14, fontSize: 14,
fontFamily: monoFontFamily(settings.appearance.font()), fontFamily: monoFontFamily(settings.appearance.font()),
allowTransparency: true, allowTransparency: true,
convertEol: true,
theme: terminalColors(), theme: terminalColors(),
scrollback: 10_000, scrollback: 10_000,
ghostty: g, ghostty: g,
@@ -236,9 +257,13 @@ export const Terminal = (props: TerminalProps) => {
serializeAddon = serializer serializeAddon = serializer
t.open(container) t.open(container)
container.addEventListener("pointerdown", handlePointerDown) container.addEventListener("pointerdown", handlePointerDown)
cleanups.push(() => container.removeEventListener("pointerdown", handlePointerDown)) cleanups.push(() => container.removeEventListener("pointerdown", handlePointerDown))
container.addEventListener("click", handleLinkClick, { capture: true })
cleanups.push(() => container.removeEventListener("click", handleLinkClick, { capture: true }))
handleTextareaFocus = () => { handleTextareaFocus = () => {
t.options.cursorBlink = true t.options.cursorBlink = true
} }
@@ -253,15 +278,11 @@ export const Terminal = (props: TerminalProps) => {
focusTerminal() focusTerminal()
fit.fit()
if (local.pty.buffer) { if (local.pty.buffer) {
if (local.pty.rows && local.pty.cols) {
t.resize(local.pty.cols, local.pty.rows)
}
t.write(local.pty.buffer, () => { t.write(local.pty.buffer, () => {
if (local.pty.scrollY) { if (local.pty.scrollY) t.scrollToLine(local.pty.scrollY)
t.scrollToLine(local.pty.scrollY)
}
fitAddon.fit()
}) })
} }
@@ -299,6 +320,19 @@ export const Terminal = (props: TerminalProps) => {
// console.log("Scroll position:", ydisp) // console.log("Scroll position:", ydisp)
// }) // })
const limit = 16_384
const seed = tail
let sync = !!seed
const overlap = (data: string) => {
if (!seed) return 0
const max = Math.min(seed.length, data.length)
for (let i = max; i > 0; i--) {
if (seed.slice(-i) === data.slice(0, i)) return i
}
return 0
}
const handleOpen = () => { const handleOpen = () => {
local.onConnect?.() local.onConnect?.()
sdk.client.pty sdk.client.pty
@@ -315,7 +349,25 @@ export const Terminal = (props: TerminalProps) => {
cleanups.push(() => socket.removeEventListener("open", handleOpen)) cleanups.push(() => socket.removeEventListener("open", handleOpen))
const handleMessage = (event: MessageEvent) => { const handleMessage = (event: MessageEvent) => {
t.write(event.data) const data = typeof event.data === "string" ? event.data : ""
if (!data) return
const next = (() => {
if (!sync) return data
const n = overlap(data)
if (!n) {
sync = false
return data
}
const trimmed = data.slice(n)
if (trimmed) sync = false
return trimmed
})()
if (!next) return
t.write(next)
tail = next.length >= limit ? next.slice(-limit) : (tail + next).slice(-limit)
} }
socket.addEventListener("message", handleMessage) socket.addEventListener("message", handleMessage)
cleanups.push(() => socket.removeEventListener("message", handleMessage)) cleanups.push(() => socket.removeEventListener("message", handleMessage))
@@ -369,6 +421,7 @@ export const Terminal = (props: TerminalProps) => {
props.onCleanup({ props.onCleanup({
...local.pty, ...local.pty,
buffer, buffer,
tail,
rows: t.rows, rows: t.rows,
cols: t.cols, cols: t.cols,
scrollY: t.getViewportY(), scrollY: t.getViewportY(),
+4 -10
View File
@@ -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" />
+10
View File
@@ -70,6 +70,14 @@ function createCommentSession(dir: string, id: string | undefined) {
setFocus((current) => (current?.id === id ? null : current)) setFocus((current) => (current?.id === id ? null : current))
} }
const clear = () => {
batch(() => {
setStore("comments", {})
setFocus(null)
setActive(null)
})
}
const all = createMemo(() => { const all = createMemo(() => {
const files = Object.keys(store.comments) const files = Object.keys(store.comments)
const items = files.flatMap((file) => store.comments[file] ?? []) const items = files.flatMap((file) => store.comments[file] ?? [])
@@ -82,6 +90,7 @@ function createCommentSession(dir: string, id: string | undefined) {
all, all,
add, add,
remove, remove,
clear,
focus: createMemo(() => state.focus), focus: createMemo(() => state.focus),
setFocus, setFocus,
clearFocus: () => setFocus(null), clearFocus: () => setFocus(null),
@@ -144,6 +153,7 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont
all: () => session().all(), all: () => session().all(),
add: (input: Omit<LineComment, "id" | "time">) => session().add(input), add: (input: Omit<LineComment, "id" | "time">) => session().add(input),
remove: (file: string, id: string) => session().remove(file, id), remove: (file: string, id: string) => session().remove(file, id),
clear: () => session().clear(),
focus: () => session().focus(), focus: () => session().focus(),
setFocus: (focus: CommentFocus | null) => session().setFocus(focus), setFocus: (focus: CommentFocus | null) => session().setFocus(focus),
clearFocus: () => session().clearFocus(), clearFocus: () => session().clearFocus(),
+11 -9
View File
@@ -1,5 +1,5 @@
import { createEffect, createMemo, createRoot, onCleanup } from "solid-js" import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js"
import { createStore, produce } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import type { FileContent, FileNode } from "@opencode-ai/sdk/v2" import type { FileContent, FileNode } from "@opencode-ai/sdk/v2"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
@@ -277,10 +277,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
const scope = createMemo(() => sdk.directory) const scope = createMemo(() => sdk.directory)
const directory = createMemo(() => sync.data.path.directory)
function normalize(input: string) { function normalize(input: string) {
const root = directory() const root = scope()
const prefix = root.endsWith("/") ? root : root + "/" const prefix = root.endsWith("/") ? root : root + "/"
let path = unquoteGitPath(stripQueryAndHash(stripFileProtocol(input))) let path = unquoteGitPath(stripQueryAndHash(stripFileProtocol(input)))
@@ -371,9 +369,13 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
inflight.clear() inflight.clear()
treeInflight.clear() treeInflight.clear()
contentLru.clear() contentLru.clear()
setStore("file", {})
setTree("node", {}) batch(() => {
setTree("dir", { "": { expanded: true } }) setStore("file", reconcile({}))
setTree("node", reconcile({}))
setTree("dir", reconcile({}))
setTree("dir", "", { expanded: true })
})
}) })
const viewCache = new Map<string, ViewCacheEntry>() const viewCache = new Map<string, ViewCacheEntry>()
@@ -414,7 +416,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
return entry.value return entry.value
} }
const view = createMemo(() => loadView(params.dir!, params.id)) const view = createMemo(() => loadView(scope(), params.id))
function ensure(path: string) { function ensure(path: string) {
if (!path) return if (!path) return
+9 -7
View File
@@ -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)
}), }),
), ),
+8
View File
@@ -18,6 +18,7 @@ import { dict as ar } from "@/i18n/ar"
import { dict as no } from "@/i18n/no" import { dict as no } from "@/i18n/no"
import { dict as br } from "@/i18n/br" import { dict as br } from "@/i18n/br"
import { dict as th } from "@/i18n/th" import { dict as th } from "@/i18n/th"
import { dict as bs } from "@/i18n/bs"
import { dict as uiEn } from "@opencode-ai/ui/i18n/en" import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh" import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht" import { dict as uiZht } from "@opencode-ai/ui/i18n/zht"
@@ -33,6 +34,7 @@ import { dict as uiAr } from "@opencode-ai/ui/i18n/ar"
import { dict as uiNo } from "@opencode-ai/ui/i18n/no" import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
import { dict as uiBr } from "@opencode-ai/ui/i18n/br" import { dict as uiBr } from "@opencode-ai/ui/i18n/br"
import { dict as uiTh } from "@opencode-ai/ui/i18n/th" import { dict as uiTh } from "@opencode-ai/ui/i18n/th"
import { dict as uiBs } from "@opencode-ai/ui/i18n/bs"
export type Locale = export type Locale =
| "en" | "en"
@@ -50,6 +52,7 @@ export type Locale =
| "no" | "no"
| "br" | "br"
| "th" | "th"
| "bs"
type RawDictionary = typeof en & typeof uiEn type RawDictionary = typeof en & typeof uiEn
type Dictionary = i18n.Flatten<RawDictionary> type Dictionary = i18n.Flatten<RawDictionary>
@@ -66,6 +69,7 @@ const LOCALES: readonly Locale[] = [
"ja", "ja",
"pl", "pl",
"ru", "ru",
"bs",
"ar", "ar",
"no", "no",
"br", "br",
@@ -99,6 +103,7 @@ function detectLocale(): Locale {
return "no" return "no"
if (language.toLowerCase().startsWith("pt")) return "br" if (language.toLowerCase().startsWith("pt")) return "br"
if (language.toLowerCase().startsWith("th")) return "th" if (language.toLowerCase().startsWith("th")) return "th"
if (language.toLowerCase().startsWith("bs")) return "bs"
} }
return "en" return "en"
@@ -129,6 +134,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
if (store.locale === "no") return "no" if (store.locale === "no") return "no"
if (store.locale === "br") return "br" if (store.locale === "br") return "br"
if (store.locale === "th") return "th" if (store.locale === "th") return "th"
if (store.locale === "bs") return "bs"
return "en" return "en"
}) })
@@ -154,6 +160,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) } if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) }
if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) } if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) }
if (locale() === "th") return { ...base, ...i18n.flatten({ ...th, ...uiTh }) } if (locale() === "th") return { ...base, ...i18n.flatten({ ...th, ...uiTh }) }
if (locale() === "bs") return { ...base, ...i18n.flatten({ ...bs, ...uiBs }) }
return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) } return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }
}) })
@@ -175,6 +182,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
no: "language.no", no: "language.no",
br: "language.br", br: "language.br",
th: "language.th", th: "language.th",
bs: "language.bs",
} }
const label = (value: Locale) => t(labelKey[value]) const label = (value: Locale) => t(labelKey[value])
+129 -9
View File
@@ -33,6 +33,14 @@ type SessionTabs = {
type SessionView = { type SessionView = {
scroll: Record<string, SessionScroll> scroll: Record<string, SessionScroll>
reviewOpen?: string[] reviewOpen?: string[]
pendingMessage?: string
pendingMessageAt?: number
}
type TabHandoff = {
dir: string
id: string
at: number
} }
export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean } export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
@@ -63,6 +71,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
} }
})() })()
const review = value.review
const fileTree = value.fileTree const fileTree = value.fileTree
const migratedFileTree = (() => { const migratedFileTree = (() => {
if (!isRecord(fileTree)) return fileTree if (!isRecord(fileTree)) return fileTree
@@ -77,10 +86,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
} }
})() })()
if (migratedSidebar === sidebar && migratedFileTree === fileTree) return value const migratedReview = (() => {
if (!isRecord(review)) return review
if (typeof review.panelOpened === "boolean") return review
const opened = isRecord(fileTree) && typeof fileTree.opened === "boolean" ? fileTree.opened : true
return {
...review,
panelOpened: opened,
}
})()
if (migratedSidebar === sidebar && migratedReview === review && migratedFileTree === fileTree) return value
return { return {
...value, ...value,
sidebar: migratedSidebar, sidebar: migratedSidebar,
review: migratedReview,
fileTree: migratedFileTree, fileTree: migratedFileTree,
} }
} }
@@ -101,6 +122,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
review: { review: {
diffStyle: "split" as ReviewDiffStyle, diffStyle: "split" as ReviewDiffStyle,
panelOpened: true,
}, },
fileTree: { fileTree: {
opened: true, opened: true,
@@ -115,10 +137,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
sessionTabs: {} as Record<string, SessionTabs>, sessionTabs: {} as Record<string, SessionTabs>,
sessionView: {} as Record<string, SessionView>, sessionView: {} as Record<string, SessionView>,
handoff: {
tabs: undefined as TabHandoff | undefined,
},
}), }),
) )
const MAX_SESSION_KEYS = 50 const MAX_SESSION_KEYS = 50
const PENDING_MESSAGE_TTL_MS = 2 * 60 * 1000
const meta = { active: undefined as string | undefined, pruned: false } const meta = { active: undefined as string | undefined, pruned: false }
const used = new Map<string, number>() const used = new Map<string, number>()
@@ -411,6 +437,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
return { return {
ready, ready,
handoff: {
tabs: createMemo(() => store.handoff?.tabs),
setTabs(dir: string, id: string) {
setStore("handoff", "tabs", { dir, id, at: Date.now() })
},
clearTabs() {
if (!store.handoff?.tabs) return
setStore("handoff", "tabs", undefined)
},
},
projects: { projects: {
list, list,
open(directory: string) { open(directory: string) {
@@ -468,7 +504,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
diffStyle: createMemo(() => store.review?.diffStyle ?? "split"), diffStyle: createMemo(() => store.review?.diffStyle ?? "split"),
setDiffStyle(diffStyle: ReviewDiffStyle) { setDiffStyle(diffStyle: ReviewDiffStyle) {
if (!store.review) { if (!store.review) {
setStore("review", { diffStyle }) setStore("review", { diffStyle, panelOpened: true })
return return
} }
setStore("review", "diffStyle", diffStyle) setStore("review", "diffStyle", diffStyle)
@@ -536,6 +572,49 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("mobileSidebar", "opened", (x) => !x) setStore("mobileSidebar", "opened", (x) => !x)
}, },
}, },
pendingMessage: {
set(sessionKey: string, messageID: string) {
const at = Date.now()
touch(sessionKey)
const current = store.sessionView[sessionKey]
if (!current) {
setStore("sessionView", sessionKey, {
scroll: {},
pendingMessage: messageID,
pendingMessageAt: at,
})
prune(meta.active ?? sessionKey)
return
}
setStore(
"sessionView",
sessionKey,
produce((draft) => {
draft.pendingMessage = messageID
draft.pendingMessageAt = at
}),
)
},
consume(sessionKey: string) {
const current = store.sessionView[sessionKey]
const message = current?.pendingMessage
const at = current?.pendingMessageAt
if (!message || !at) return
setStore(
"sessionView",
sessionKey,
produce((draft) => {
delete draft.pendingMessage
delete draft.pendingMessageAt
}),
)
if (Date.now() - at > PENDING_MESSAGE_TTL_MS) return
return message
},
},
view(sessionKey: string | Accessor<string>) { view(sessionKey: string | Accessor<string>) {
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
@@ -555,6 +634,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} }) const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
const terminalOpened = createMemo(() => store.terminal?.opened ?? false) const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
function setTerminalOpened(next: boolean) { function setTerminalOpened(next: boolean) {
const current = store.terminal const current = store.terminal
@@ -568,6 +648,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("terminal", "opened", next) setStore("terminal", "opened", next)
} }
function setReviewPanelOpened(next: boolean) {
const current = store.review
if (!current) {
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next })
return
}
const value = current.panelOpened ?? true
if (value === next) return
setStore("review", "panelOpened", next)
}
return { return {
scroll(tab: string) { scroll(tab: string) {
return scroll.scroll(key(), tab) return scroll.scroll(key(), tab)
@@ -587,6 +679,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setTerminalOpened(!terminalOpened()) setTerminalOpened(!terminalOpened())
}, },
}, },
reviewPanel: {
opened: reviewPanelOpened,
open() {
setReviewPanelOpened(true)
},
close() {
setReviewPanelOpened(false)
},
toggle() {
setReviewPanelOpened(!reviewPanelOpened())
},
},
review: { review: {
open: createMemo(() => s().reviewOpen), open: createMemo(() => s().reviewOpen),
setOpen(open: string[]) { setOpen(open: string[]) {
@@ -624,11 +728,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] }) const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
return { return {
tabs, tabs,
active: createMemo(() => (tabs().active === "review" ? undefined : tabs().active)), active: createMemo(() => tabs().active),
all: createMemo(() => tabs().all.filter((tab) => tab !== "review")), all: createMemo(() => tabs().all.filter((tab) => tab !== "review")),
setActive(tab: string | undefined) { setActive(tab: string | undefined) {
const session = key() const session = key()
if (tab === "review") return
if (!store.sessionTabs[session]) { if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all: [], active: tab }) setStore("sessionTabs", session, { all: [], active: tab })
} else { } else {
@@ -645,10 +748,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
} }
}, },
async open(tab: string) { async open(tab: string) {
if (tab === "review") return
const session = key() const session = key()
const current = store.sessionTabs[session] ?? { all: [] } const current = store.sessionTabs[session] ?? { all: [] }
if (tab === "review") {
if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all: current.all.filter((x) => x !== "review"), active: tab })
return
}
setStore("sessionTabs", session, "active", tab)
return
}
if (tab === "context") { if (tab === "context") {
const all = [tab, ...current.all.filter((x) => x !== tab)] const all = [tab, ...current.all.filter((x) => x !== tab)]
if (!store.sessionTabs[session]) { if (!store.sessionTabs[session]) {
@@ -681,13 +792,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const current = store.sessionTabs[session] const current = store.sessionTabs[session]
if (!current) return if (!current) return
if (tab === "review") {
if (current.active !== tab) return
setStore("sessionTabs", session, "active", current.all[0])
return
}
const all = current.all.filter((x) => x !== tab) 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)
}) })
}, },
+3
View File
@@ -15,6 +15,9 @@ export type Platform = {
/** Open a URL in the default browser */ /** Open a URL in the default browser */
openLink(url: string): void openLink(url: string): void
/** Open a local path in a local app (desktop only) */
openPath?(path: string, app?: string): Promise<void>
/** Restart the app */ /** Restart the app */
restart(): Promise<void> restart(): Promise<void>
+3 -3
View File
@@ -1,17 +1,17 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client" import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus" import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { createEffect, createMemo, onCleanup } from "solid-js" import { createEffect, createMemo, onCleanup, type Accessor } from "solid-js"
import { useGlobalSDK } from "./global-sdk" import { useGlobalSDK } from "./global-sdk"
import { usePlatform } from "./platform" import { usePlatform } from "./platform"
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
name: "SDK", name: "SDK",
init: (props: { directory: string }) => { init: (props: { directory: Accessor<string> }) => {
const platform = usePlatform() const platform = usePlatform()
const globalSDK = useGlobalSDK() const globalSDK = useGlobalSDK()
const directory = createMemo(() => props.directory) const directory = createMemo(props.directory)
const client = createMemo(() => const client = createMemo(() =>
createOpencodeClient({ createOpencodeClient({
baseUrl: globalSDK.url, baseUrl: globalSDK.url,
+6 -4
View File
@@ -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" }))
}) })
+1
View File
@@ -13,6 +13,7 @@ export type LocalPTY = {
cols?: number cols?: number
buffer?: string buffer?: string
scrollY?: number scrollY?: number
tail?: string
} }
const WORKSPACE_KEY = "__workspace__" const WORKSPACE_KEY = "__workspace__"
+7 -18
View File
@@ -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": "الملفات",
@@ -321,22 +321,6 @@ export const dict = {
"context.usage.clickToView": "انقر لعرض السياق", "context.usage.clickToView": "انقر لعرض السياق",
"context.usage.view": "عرض استخدام السياق", "context.usage.view": "عرض استخدام السياق",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "لغة", "toast.language.title": "لغة",
"toast.language.description": "تم التبديل إلى {{language}}", "toast.language.description": "تم التبديل إلى {{language}}",
@@ -348,6 +332,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": "قم بتوصيل موفر لتلخيص هذه الجلسة",
+7 -18
View File
@@ -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",
@@ -320,22 +320,6 @@ export const dict = {
"context.usage.clickToView": "Clique para ver o contexto", "context.usage.clickToView": "Clique para ver o contexto",
"context.usage.view": "Ver uso do contexto", "context.usage.view": "Ver uso do contexto",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Idioma", "toast.language.title": "Idioma",
"toast.language.description": "Alterado para {{language}}", "toast.language.description": "Alterado para {{language}}",
@@ -347,6 +331,11 @@ export const dict = {
"toast.permissions.autoaccept.off.title": "Parou de aceitar edições automaticamente", "toast.permissions.autoaccept.off.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",
+747
View File
@@ -0,0 +1,747 @@
export const dict = {
"command.category.suggested": "Predloženo",
"command.category.view": "Prikaz",
"command.category.project": "Projekat",
"command.category.provider": "Provajder",
"command.category.server": "Server",
"command.category.session": "Sesija",
"command.category.theme": "Tema",
"command.category.language": "Jezik",
"command.category.file": "Datoteka",
"command.category.context": "Kontekst",
"command.category.terminal": "Terminal",
"command.category.model": "Model",
"command.category.mcp": "MCP",
"command.category.agent": "Agent",
"command.category.permissions": "Dozvole",
"command.category.workspace": "Radni prostor",
"command.category.settings": "Postavke",
"theme.scheme.system": "Sistem",
"theme.scheme.light": "Svijetlo",
"theme.scheme.dark": "Tamno",
"command.sidebar.toggle": "Prikaži/sakrij bočnu traku",
"command.project.open": "Otvori projekat",
"command.provider.connect": "Poveži provajdera",
"command.server.switch": "Promijeni server",
"command.settings.open": "Otvori postavke",
"command.session.previous": "Prethodna sesija",
"command.session.next": "Sljedeća sesija",
"command.session.previous.unseen": "Prethodna nepročitana sesija",
"command.session.next.unseen": "Sljedeća nepročitana sesija",
"command.session.archive": "Arhiviraj sesiju",
"command.palette": "Paleta komandi",
"command.theme.cycle": "Promijeni temu",
"command.theme.set": "Koristi temu: {{theme}}",
"command.theme.scheme.cycle": "Promijeni šemu boja",
"command.theme.scheme.set": "Koristi šemu boja: {{scheme}}",
"command.language.cycle": "Promijeni jezik",
"command.language.set": "Koristi jezik: {{language}}",
"command.session.new": "Nova sesija",
"command.file.open": "Otvori datoteku",
"command.tab.close": "Zatvori karticu",
"command.context.addSelection": "Dodaj odabir u kontekst",
"command.context.addSelection.description": "Dodaj odabrane linije iz trenutne datoteke",
"command.terminal.toggle": "Prikaži/sakrij terminal",
"command.fileTree.toggle": "Prikaži/sakrij stablo datoteka",
"command.review.toggle": "Prikaži/sakrij pregled",
"command.terminal.new": "Novi terminal",
"command.terminal.new.description": "Kreiraj novu karticu terminala",
"command.steps.toggle": "Prikaži/sakrij korake",
"command.steps.toggle.description": "Prikaži ili sakrij korake za trenutnu poruku",
"command.message.previous": "Prethodna poruka",
"command.message.previous.description": "Idi na prethodnu korisničku poruku",
"command.message.next": "Sljedeća poruka",
"command.message.next.description": "Idi na sljedeću korisničku poruku",
"command.model.choose": "Odaberi model",
"command.model.choose.description": "Odaberi drugi model",
"command.mcp.toggle": "Prikaži/sakrij MCP-ove",
"command.mcp.toggle.description": "Prikaži/sakrij MCP-ove",
"command.agent.cycle": "Promijeni agenta",
"command.agent.cycle.description": "Prebaci na sljedećeg agenta",
"command.agent.cycle.reverse": "Promijeni agenta unazad",
"command.agent.cycle.reverse.description": "Prebaci na prethodnog agenta",
"command.model.variant.cycle": "Promijeni nivo razmišljanja",
"command.model.variant.cycle.description": "Prebaci na sljedeći nivo",
"command.permissions.autoaccept.enable": "Automatski prihvataj izmjene",
"command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje izmjena",
"command.workspace.toggle": "Prikaži/sakrij radne prostore",
"command.workspace.toggle.description": "Omogući ili onemogući više radnih prostora u bočnoj traci",
"command.session.undo": "Poništi",
"command.session.undo.description": "Poništi posljednju poruku",
"command.session.redo": "Vrati",
"command.session.redo.description": "Vrati posljednju poništenu poruku",
"command.session.compact": "Sažmi sesiju",
"command.session.compact.description": "Sažmi sesiju kako bi se smanjio kontekst",
"command.session.fork": "Fork iz poruke",
"command.session.fork.description": "Kreiraj novu sesiju iz prethodne poruke",
"command.session.share": "Podijeli sesiju",
"command.session.share.description": "Podijeli ovu sesiju i kopiraj URL u međuspremnik",
"command.session.unshare": "Ukini dijeljenje sesije",
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
"palette.search.placeholder": "Pretraži datoteke, komande i sesije",
"palette.empty": "Nema rezultata",
"palette.group.commands": "Komande",
"palette.group.files": "Datoteke",
"dialog.provider.search.placeholder": "Pretraži provajdere",
"dialog.provider.empty": "Nema pronađenih provajdera",
"dialog.provider.group.popular": "Popularno",
"dialog.provider.group.other": "Ostalo",
"dialog.provider.tag.recommended": "Preporučeno",
"dialog.provider.opencode.note": "Kurirani modeli uključujući Claude, GPT, Gemini i druge",
"dialog.provider.anthropic.note": "Direktan pristup Claude modelima, uključujući Pro i Max",
"dialog.provider.copilot.note": "Claude modeli za pomoć pri kodiranju",
"dialog.provider.openai.note": "GPT modeli za brze, sposobne opšte AI zadatke",
"dialog.provider.google.note": "Gemini modeli za brze, strukturirane odgovore",
"dialog.provider.openrouter.note": "Pristup svim podržanim modelima preko jednog provajdera",
"dialog.provider.vercel.note": "Jedinstven pristup AI modelima uz pametno rutiranje",
"dialog.model.select.title": "Odaberi model",
"dialog.model.search.placeholder": "Pretraži modele",
"dialog.model.empty": "Nema rezultata za modele",
"dialog.model.manage": "Upravljaj modelima",
"dialog.model.manage.description": "Prilagodi koji se modeli prikazuju u izborniku modela.",
"dialog.model.unpaid.freeModels.title": "Besplatni modeli koje obezbjeđuje OpenCode",
"dialog.model.unpaid.addMore.title": "Dodaj još modela od popularnih provajdera",
"dialog.provider.viewAll": "Prikaži više provajdera",
"provider.connect.title": "Poveži {{provider}}",
"provider.connect.title.anthropicProMax": "Prijavi se putem Claude Pro/Max",
"provider.connect.selectMethod": "Odaberi način prijave za {{provider}}.",
"provider.connect.method.apiKey": "API ključ",
"provider.connect.status.inProgress": "Autorizacija je u toku...",
"provider.connect.status.waiting": "Čekanje na autorizaciju...",
"provider.connect.status.failed": "Autorizacija nije uspjela: {{error}}",
"provider.connect.apiKey.description":
"Unesi svoj {{provider}} API ključ da povežeš račun i koristiš {{provider}} modele u OpenCode-u.",
"provider.connect.apiKey.label": "{{provider}} API ključ",
"provider.connect.apiKey.placeholder": "API ključ",
"provider.connect.apiKey.required": "API ključ je obavezan",
"provider.connect.opencodeZen.line1":
"OpenCode Zen ti daje pristup kuriranom skupu pouzdanih, optimizovanih modela za coding agente.",
"provider.connect.opencodeZen.line2":
"Sa jednim API ključem dobijaš pristup modelima kao što su Claude, GPT, Gemini, GLM i drugi.",
"provider.connect.opencodeZen.visit.prefix": "Posjeti ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": " da preuzmeš svoj API ključ.",
"provider.connect.oauth.code.visit.prefix": "Posjeti ",
"provider.connect.oauth.code.visit.link": "ovaj link",
"provider.connect.oauth.code.visit.suffix":
" da preuzmeš autorizacijski kod i povežeš račun te koristiš {{provider}} modele u OpenCode-u.",
"provider.connect.oauth.code.label": "{{method}} autorizacijski kod",
"provider.connect.oauth.code.placeholder": "Autorizacijski kod",
"provider.connect.oauth.code.required": "Autorizacijski kod je obavezan",
"provider.connect.oauth.code.invalid": "Nevažeći autorizacijski kod",
"provider.connect.oauth.auto.visit.prefix": "Posjeti ",
"provider.connect.oauth.auto.visit.link": "ovaj link",
"provider.connect.oauth.auto.visit.suffix":
" i unesi kod ispod da povežeš račun i koristiš {{provider}} modele u OpenCode-u.",
"provider.connect.oauth.auto.confirmationCode": "Kod za potvrdu",
"provider.connect.toast.connected.title": "{{provider}} povezan",
"provider.connect.toast.connected.description": "{{provider}} modeli su sada dostupni za korištenje.",
"provider.disconnect.toast.disconnected.title": "{{provider}} odspojen",
"provider.disconnect.toast.disconnected.description": "{{provider}} modeli više nisu dostupni.",
"model.tag.free": "Besplatno",
"model.tag.latest": "Najnovije",
"model.provider.anthropic": "Anthropic",
"model.provider.openai": "OpenAI",
"model.provider.google": "Google",
"model.provider.xai": "xAI",
"model.provider.meta": "Meta",
"model.input.text": "tekst",
"model.input.image": "slika",
"model.input.audio": "zvuk",
"model.input.video": "video",
"model.input.pdf": "pdf",
"model.tooltip.allows": "Podržava: {{inputs}}",
"model.tooltip.reasoning.allowed": "Podržava rasuđivanje",
"model.tooltip.reasoning.none": "Bez rasuđivanja",
"model.tooltip.context": "Limit konteksta {{limit}}",
"common.search.placeholder": "Pretraži",
"common.goBack": "Nazad",
"common.goForward": "Naprijed",
"common.loading": "Učitavanje",
"common.loading.ellipsis": "...",
"common.cancel": "Otkaži",
"common.connect": "Poveži",
"common.disconnect": "Prekini vezu",
"common.submit": "Pošalji",
"common.save": "Sačuvaj",
"common.saving": "Čuvanje...",
"common.default": "Podrazumijevano",
"common.attachment": "prilog",
"prompt.placeholder.shell": "Unesi shell naredbu...",
"prompt.placeholder.normal": 'Pitaj bilo šta... "{{example}}"',
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
"prompt.placeholder.summarizeComment": "Sažmi komentar…",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc za izlaz",
"prompt.example.1": "Popravi TODO u bazi koda",
"prompt.example.2": "Koji je tehnološki stack ovog projekta?",
"prompt.example.3": "Popravi pokvarene testove",
"prompt.example.4": "Objasni kako radi autentifikacija",
"prompt.example.5": "Pronađi i popravi sigurnosne ranjivosti",
"prompt.example.6": "Dodaj jedinične testove za servis korisnika",
"prompt.example.7": "Refaktoriši ovu funkciju da bude čitljivija",
"prompt.example.8": "Šta znači ova greška?",
"prompt.example.9": "Pomozi mi da otklonim ovu grešku",
"prompt.example.10": "Generiši API dokumentaciju",
"prompt.example.11": "Optimizuj upite prema bazi podataka",
"prompt.example.12": "Dodaj validaciju ulaza",
"prompt.example.13": "Napravi novu komponentu za...",
"prompt.example.14": "Kako da deployam ovaj projekat?",
"prompt.example.15": "Pregledaj moj kod prema najboljim praksama",
"prompt.example.16": "Dodaj obradu grešaka u ovu funkciju",
"prompt.example.17": "Objasni ovaj regex obrazac",
"prompt.example.18": "Pretvori ovo u TypeScript",
"prompt.example.19": "Dodaj logovanje kroz cijelu bazu koda",
"prompt.example.20": "Koje su zavisnosti zastarjele?",
"prompt.example.21": "Pomozi mi da napišem migracijsku skriptu",
"prompt.example.22": "Implementiraj keširanje za ovaj endpoint",
"prompt.example.23": "Dodaj paginaciju u ovu listu",
"prompt.example.24": "Napravi CLI komandu za...",
"prompt.example.25": "Kako ovdje rade varijable okruženja?",
"prompt.popover.emptyResults": "Nema rezultata",
"prompt.popover.emptyCommands": "Nema komandi",
"prompt.dropzone.label": "Spusti slike ili PDF-ove ovdje",
"prompt.slash.badge.custom": "prilagođeno",
"prompt.slash.badge.skill": "skill",
"prompt.slash.badge.mcp": "mcp",
"prompt.context.active": "aktivno",
"prompt.context.includeActiveFile": "Uključi aktivnu datoteku",
"prompt.context.removeActiveFile": "Ukloni aktivnu datoteku iz konteksta",
"prompt.context.removeFile": "Ukloni datoteku iz konteksta",
"prompt.action.attachFile": "Priloži datoteku",
"prompt.attachment.remove": "Ukloni prilog",
"prompt.action.send": "Pošalji",
"prompt.action.stop": "Zaustavi",
"prompt.toast.pasteUnsupported.title": "Nepodržano lijepljenje",
"prompt.toast.pasteUnsupported.description": "Ovdje se mogu zalijepiti samo slike ili PDF-ovi.",
"prompt.toast.modelAgentRequired.title": "Odaberi agenta i model",
"prompt.toast.modelAgentRequired.description": "Odaberi agenta i model prije slanja upita.",
"prompt.toast.worktreeCreateFailed.title": "Neuspješno kreiranje worktree-a",
"prompt.toast.sessionCreateFailed.title": "Neuspješno kreiranje sesije",
"prompt.toast.shellSendFailed.title": "Neuspješno slanje shell naredbe",
"prompt.toast.commandSendFailed.title": "Neuspješno slanje komande",
"prompt.toast.promptSendFailed.title": "Neuspješno slanje upita",
"dialog.mcp.title": "MCP-ovi",
"dialog.mcp.description": "{{enabled}} od {{total}} omogućeno",
"dialog.mcp.empty": "Nema konfigurisnih MCP-ova",
"dialog.lsp.empty": "LSP-ovi se automatski otkrivaju prema tipu datoteke",
"dialog.plugins.empty": "Plugini su konfigurisani u opencode.json",
"mcp.status.connected": "povezano",
"mcp.status.failed": "neuspjelo",
"mcp.status.needs_auth": "potrebna autentifikacija",
"mcp.status.disabled": "onemogućeno",
"dialog.fork.empty": "Nema poruka za fork",
"dialog.directory.search.placeholder": "Pretraži foldere",
"dialog.directory.empty": "Nema pronađenih foldera",
"dialog.server.title": "Serveri",
"dialog.server.description": "Promijeni na koji se OpenCode server ova aplikacija povezuje.",
"dialog.server.search.placeholder": "Pretraži servere",
"dialog.server.empty": "Još nema servera",
"dialog.server.add.title": "Dodaj server",
"dialog.server.add.url": "URL servera",
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "Nije moguće povezati se na server",
"dialog.server.add.checking": "Provjera...",
"dialog.server.add.button": "Dodaj server",
"dialog.server.default.title": "Podrazumijevani server",
"dialog.server.default.description":
"Poveži se na ovaj server pri pokretanju aplikacije umjesto pokretanja lokalnog servera. Potreban je restart.",
"dialog.server.default.none": "Nije odabran server",
"dialog.server.default.set": "Postavi trenutni server kao podrazumijevani",
"dialog.server.default.clear": "Očisti",
"dialog.server.action.remove": "Ukloni server",
"dialog.server.menu.edit": "Uredi",
"dialog.server.menu.default": "Postavi kao podrazumijevano",
"dialog.server.menu.defaultRemove": "Ukloni podrazumijevano",
"dialog.server.menu.delete": "Izbriši",
"dialog.server.current": "Trenutni server",
"dialog.server.status.default": "Podrazumijevano",
"dialog.project.edit.title": "Uredi projekat",
"dialog.project.edit.name": "Naziv",
"dialog.project.edit.icon": "Ikonica",
"dialog.project.edit.icon.alt": "Ikonica projekta",
"dialog.project.edit.icon.hint": "Klikni ili prevuci sliku",
"dialog.project.edit.icon.recommended": "Preporučeno: 128x128px",
"dialog.project.edit.color": "Boja",
"dialog.project.edit.color.select": "Odaberi boju {{color}}",
"dialog.project.edit.worktree.startup": "Skripta za pokretanje radnog prostora",
"dialog.project.edit.worktree.startup.description": "Pokreće se nakon kreiranja novog radnog prostora (worktree).",
"dialog.project.edit.worktree.startup.placeholder": "npr. bun install",
"context.breakdown.title": "Razlaganje konteksta",
"context.breakdown.note":
'Približna raspodjela ulaznih tokena. "Ostalo" uključuje definicije alata i dodatni overhead.',
"context.breakdown.system": "Sistem",
"context.breakdown.user": "Korisnik",
"context.breakdown.assistant": "Asistent",
"context.breakdown.tool": "Pozivi alata",
"context.breakdown.other": "Ostalo",
"context.systemPrompt.title": "Sistemski prompt",
"context.rawMessages.title": "Sirove poruke",
"context.stats.session": "Sesija",
"context.stats.messages": "Poruke",
"context.stats.provider": "Provajder",
"context.stats.model": "Model",
"context.stats.limit": "Limit konteksta",
"context.stats.totalTokens": "Ukupno tokena",
"context.stats.usage": "Korištenje",
"context.stats.inputTokens": "Ulazni tokeni",
"context.stats.outputTokens": "Izlazni tokeni",
"context.stats.reasoningTokens": "Tokeni za rasuđivanje",
"context.stats.cacheTokens": "Cache tokeni (čitanje/pisanje)",
"context.stats.userMessages": "Korisničke poruke",
"context.stats.assistantMessages": "Poruke asistenta",
"context.stats.totalCost": "Ukupni trošak",
"context.stats.sessionCreated": "Sesija kreirana",
"context.stats.lastActivity": "Posljednja aktivnost",
"context.usage.tokens": "Tokeni",
"context.usage.usage": "Korištenje",
"context.usage.cost": "Trošak",
"context.usage.clickToView": "Klikni da vidiš kontekst",
"context.usage.view": "Prikaži korištenje konteksta",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.bs": "Bosanski",
"language.th": "ไทย",
"toast.language.title": "Jezik",
"toast.language.description": "Prebačeno na {{language}}",
"toast.theme.title": "Tema promijenjena",
"toast.scheme.title": "Šema boja",
"toast.workspace.enabled.title": "Radni prostori omogućeni",
"toast.workspace.enabled.description": "Više worktree-ova se sada prikazuje u bočnoj traci",
"toast.workspace.disabled.title": "Radni prostori onemogućeni",
"toast.workspace.disabled.description": "Samo glavni worktree se prikazuje u bočnoj traci",
"toast.permissions.autoaccept.on.title": "Automatsko prihvatanje izmjena",
"toast.permissions.autoaccept.on.description": "Dozvole za izmjene i pisanje biće automatski odobrene",
"toast.permissions.autoaccept.off.title": "Zaustavljeno automatsko prihvatanje izmjena",
"toast.permissions.autoaccept.off.description": "Dozvole za izmjene i pisanje zahtijevaće odobrenje",
"toast.model.none.title": "Nije odabran model",
"toast.model.none.description": "Poveži provajdera da sažmeš ovu sesiju",
"toast.file.loadFailed.title": "Neuspjelo učitavanje datoteke",
"toast.file.listFailed.title": "Neuspješno listanje datoteka",
"toast.context.noLineSelection.title": "Nema odabranih linija",
"toast.context.noLineSelection.description": "Prvo odaberi raspon linija u kartici datoteke.",
"toast.session.share.copyFailed.title": "Neuspjelo kopiranje URL-a u međuspremnik",
"toast.session.share.success.title": "Sesija podijeljena",
"toast.session.share.success.description": "URL za dijeljenje je kopiran u međuspremnik!",
"toast.session.share.failed.title": "Neuspjelo dijeljenje sesije",
"toast.session.share.failed.description": "Došlo je do greške prilikom dijeljenja sesije",
"toast.session.unshare.success.title": "Dijeljenje sesije ukinuto",
"toast.session.unshare.success.description": "Dijeljenje sesije je uspješno ukinuto!",
"toast.session.unshare.failed.title": "Neuspjelo ukidanje dijeljenja",
"toast.session.unshare.failed.description": "Došlo je do greške prilikom ukidanja dijeljenja",
"toast.session.listFailed.title": "Neuspjelo učitavanje sesija za {{project}}",
"toast.update.title": "Dostupno ažuriranje",
"toast.update.description": "Nova verzija OpenCode-a ({{version}}) je dostupna za instalaciju.",
"toast.update.action.installRestart": "Instaliraj i restartuj",
"toast.update.action.notYet": "Ne još",
"error.page.title": "Nešto je pošlo po zlu",
"error.page.description": "Došlo je do greške prilikom učitavanja aplikacije.",
"error.page.details.label": "Detalji greške",
"error.page.action.restart": "Restartuj",
"error.page.action.checking": "Provjera...",
"error.page.action.checkUpdates": "Provjeri ažuriranja",
"error.page.action.updateTo": "Ažuriraj na {{version}}",
"error.page.report.prefix": "Molimo prijavi ovu grešku OpenCode timu",
"error.page.report.discord": "na Discordu",
"error.page.version": "Verzija: {{version}}",
"error.dev.rootNotFound":
"Korijenski element nije pronađen. Da li si zaboravio da ga dodaš u index.html? Ili je možda id atribut pogrešno napisan?",
"error.globalSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?",
"error.chain.unknown": "Nepoznata greška",
"error.chain.causedBy": "Uzrok:",
"error.chain.apiError": "API greška",
"error.chain.status": "Status: {{status}}",
"error.chain.retryable": "Može se ponoviti: {{retryable}}",
"error.chain.responseBody": "Tijelo odgovora:\n{{body}}",
"error.chain.didYouMean": "Da li si mislio: {{suggestions}}",
"error.chain.modelNotFound": "Model nije pronađen: {{provider}}/{{model}}",
"error.chain.checkConfig": "Provjeri konfiguraciju (opencode.json) - nazive provajdera/modela",
"error.chain.mcpFailed": 'MCP server "{{name}}" nije uspio. Napomena: OpenCode još ne podržava MCP autentifikaciju.',
"error.chain.providerAuthFailed": "Autentifikacija provajdera nije uspjela ({{provider}}): {{message}}",
"error.chain.providerInitFailed":
'Neuspjelo inicijalizovanje provajdera "{{provider}}". Provjeri kredencijale i konfiguraciju.',
"error.chain.configJsonInvalid": "Konfiguracijska datoteka na {{path}} nije važeći JSON(C)",
"error.chain.configJsonInvalidWithMessage": "Konfiguracijska datoteka na {{path}} nije važeći JSON(C): {{message}}",
"error.chain.configDirectoryTypo":
'Direktorij "{{dir}}" u {{path}} nije ispravan. Preimenuj direktorij u "{{suggestion}}" ili ga ukloni. Ovo je česta greška u kucanju.',
"error.chain.configFrontmatterError": "Neuspjelo parsiranje frontmatter-a u {{path}}:\n{{message}}",
"error.chain.configInvalid": "Konfiguracijska datoteka na {{path}} nije ispravna",
"error.chain.configInvalidWithMessage": "Konfiguracijska datoteka na {{path}} nije ispravna: {{message}}",
"notification.permission.title": "Potrebna dozvola",
"notification.permission.description": "{{sessionTitle}} u {{projectName}} traži dozvolu",
"notification.question.title": "Pitanje",
"notification.question.description": "{{sessionTitle}} u {{projectName}} ima pitanje",
"notification.action.goToSession": "Idi na sesiju",
"notification.session.responseReady.title": "Odgovor je spreman",
"notification.session.error.title": "Greška sesije",
"notification.session.error.fallbackDescription": "Došlo je do greške",
"home.recentProjects": "Nedavni projekti",
"home.empty.title": "Nema nedavnih projekata",
"home.empty.description": "Kreni tako što ćeš otvoriti lokalni projekat",
"session.tab.session": "Sesija",
"session.tab.review": "Pregled",
"session.tab.context": "Kontekst",
"session.panel.reviewAndFiles": "Pregled i datoteke",
"session.review.filesChanged": "Izmijenjeno {{count}} datoteka",
"session.review.change.one": "Izmjena",
"session.review.change.other": "Izmjene",
"session.review.loadingChanges": "Učitavanje izmjena...",
"session.review.empty": "Još nema izmjena u ovoj sesiji",
"session.review.noChanges": "Nema izmjena",
"session.files.selectToOpen": "Odaberi datoteku za otvaranje",
"session.files.all": "Sve datoteke",
"session.files.binaryContent": "Binarna datoteka (sadržaj se ne može prikazati)",
"session.messages.renderEarlier": "Prikaži ranije poruke",
"session.messages.loadingEarlier": "Učitavanje ranijih poruka...",
"session.messages.loadEarlier": "Učitaj ranije poruke",
"session.messages.loading": "Učitavanje poruka...",
"session.messages.jumpToLatest": "Idi na najnovije",
"session.context.addToContext": "Dodaj {{selection}} u kontekst",
"session.new.worktree.main": "Glavna grana",
"session.new.worktree.mainWithBranch": "Glavna grana ({{branch}})",
"session.new.worktree.create": "Kreiraj novi worktree",
"session.new.lastModified": "Posljednja izmjena",
"session.header.search.placeholder": "Pretraži {{project}}",
"session.header.searchFiles": "Pretraži datoteke",
"status.popover.trigger": "Status",
"status.popover.ariaLabel": "Konfiguracije servera",
"status.popover.tab.servers": "Serveri",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugini",
"status.popover.action.manageServers": "Upravljaj serverima",
"session.share.popover.title": "Objavi na webu",
"session.share.popover.description.shared": "Ova sesija je javna na webu. Dostupna je svima koji imaju link.",
"session.share.popover.description.unshared": "Podijeli sesiju javno na webu. Biće dostupna svima koji imaju link.",
"session.share.action.share": "Podijeli",
"session.share.action.publish": "Objavi",
"session.share.action.publishing": "Objavljivanje...",
"session.share.action.unpublish": "Poništi objavu",
"session.share.action.unpublishing": "Poništavanje objave...",
"session.share.action.view": "Prikaži",
"session.share.copy.copied": "Kopirano",
"session.share.copy.copyLink": "Kopiraj link",
"lsp.tooltip.none": "Nema LSP servera",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Učitavanje upita...",
"terminal.loading": "Učitavanje terminala...",
"terminal.title": "Terminal",
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Zatvori terminal",
"terminal.connectionLost.title": "Veza prekinuta",
"terminal.connectionLost.description":
"Veza s terminalom je prekinuta. Ovo se može desiti kada se server restartuje.",
"common.closeTab": "Zatvori karticu",
"common.dismiss": "Odbaci",
"common.requestFailed": "Zahtjev nije uspio",
"common.moreOptions": "Više opcija",
"common.learnMore": "Saznaj više",
"common.rename": "Preimenuj",
"common.reset": "Resetuj",
"common.archive": "Arhiviraj",
"common.delete": "Izbriši",
"common.close": "Zatvori",
"common.edit": "Uredi",
"common.loadMore": "Učitaj još",
"common.key.esc": "ESC",
"sidebar.menu.toggle": "Prikaži/sakrij meni",
"sidebar.nav.projectsAndSessions": "Projekti i sesije",
"sidebar.settings": "Postavke",
"sidebar.help": "Pomoć",
"sidebar.workspaces.enable": "Omogući radne prostore",
"sidebar.workspaces.disable": "Onemogući radne prostore",
"sidebar.gettingStarted.title": "Početak",
"sidebar.gettingStarted.line1": "OpenCode uključuje besplatne modele, tako da možeš odmah početi.",
"sidebar.gettingStarted.line2": "Poveži bilo kojeg provajdera da koristiš modele, npr. Claude, GPT, Gemini itd.",
"sidebar.project.recentSessions": "Nedavne sesije",
"sidebar.project.viewAllSessions": "Prikaži sve sesije",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Desktop",
"settings.section.server": "Server",
"settings.tab.general": "Opšte",
"settings.tab.shortcuts": "Prečice",
"settings.general.section.appearance": "Izgled",
"settings.general.section.notifications": "Sistemske obavijesti",
"settings.general.section.updates": "Ažuriranja",
"settings.general.section.sounds": "Zvučni efekti",
"settings.general.row.language.title": "Jezik",
"settings.general.row.language.description": "Promijeni jezik prikaza u OpenCode-u",
"settings.general.row.appearance.title": "Izgled",
"settings.general.row.appearance.description": "Prilagodi kako OpenCode izgleda na tvom uređaju",
"settings.general.row.theme.title": "Tema",
"settings.general.row.theme.description": "Prilagodi temu OpenCode-a.",
"settings.general.row.font.title": "Font",
"settings.general.row.font.description": "Prilagodi monospace font koji se koristi u blokovima koda",
"settings.general.row.releaseNotes.title": "Bilješke o izdanju",
"settings.general.row.releaseNotes.description": 'Prikaži iskačuće prozore "Šta je novo" nakon ažuriranja',
"settings.updates.row.startup.title": "Provjeri ažuriranja pri pokretanju",
"settings.updates.row.startup.description": "Automatski provjerava ažuriranja kada se OpenCode pokrene",
"settings.updates.row.check.title": "Provjeri ažuriranja",
"settings.updates.row.check.description": "Ručno provjeri ažuriranja i instaliraj ako su dostupna",
"settings.updates.action.checkNow": "Provjeri sada",
"settings.updates.action.checking": "Provjera...",
"settings.updates.toast.latest.title": "Sve je ažurno",
"settings.updates.toast.latest.description": "Koristiš najnoviju verziju OpenCode-a.",
"font.option.ibmPlexMono": "IBM Plex Mono",
"font.option.cascadiaCode": "Cascadia Code",
"font.option.firaCode": "Fira Code",
"font.option.hack": "Hack",
"font.option.inconsolata": "Inconsolata",
"font.option.intelOneMono": "Intel One Mono",
"font.option.iosevka": "Iosevka",
"font.option.jetbrainsMono": "JetBrains Mono",
"font.option.mesloLgs": "Meslo LGS",
"font.option.robotoMono": "Roboto Mono",
"font.option.sourceCodePro": "Source Code Pro",
"font.option.ubuntuMono": "Ubuntu Mono",
"sound.option.alert01": "Upozorenje 01",
"sound.option.alert02": "Upozorenje 02",
"sound.option.alert03": "Upozorenje 03",
"sound.option.alert04": "Upozorenje 04",
"sound.option.alert05": "Upozorenje 05",
"sound.option.alert06": "Upozorenje 06",
"sound.option.alert07": "Upozorenje 07",
"sound.option.alert08": "Upozorenje 08",
"sound.option.alert09": "Upozorenje 09",
"sound.option.alert10": "Upozorenje 10",
"sound.option.bipbop01": "Bip-bop 01",
"sound.option.bipbop02": "Bip-bop 02",
"sound.option.bipbop03": "Bip-bop 03",
"sound.option.bipbop04": "Bip-bop 04",
"sound.option.bipbop05": "Bip-bop 05",
"sound.option.bipbop06": "Bip-bop 06",
"sound.option.bipbop07": "Bip-bop 07",
"sound.option.bipbop08": "Bip-bop 08",
"sound.option.bipbop09": "Bip-bop 09",
"sound.option.bipbop10": "Bip-bop 10",
"sound.option.staplebops01": "Staplebops 01",
"sound.option.staplebops02": "Staplebops 02",
"sound.option.staplebops03": "Staplebops 03",
"sound.option.staplebops04": "Staplebops 04",
"sound.option.staplebops05": "Staplebops 05",
"sound.option.staplebops06": "Staplebops 06",
"sound.option.staplebops07": "Staplebops 07",
"sound.option.nope01": "Ne 01",
"sound.option.nope02": "Ne 02",
"sound.option.nope03": "Ne 03",
"sound.option.nope04": "Ne 04",
"sound.option.nope05": "Ne 05",
"sound.option.nope06": "Ne 06",
"sound.option.nope07": "Ne 07",
"sound.option.nope08": "Ne 08",
"sound.option.nope09": "Ne 09",
"sound.option.nope10": "Ne 10",
"sound.option.nope11": "Ne 11",
"sound.option.nope12": "Ne 12",
"sound.option.yup01": "Da 01",
"sound.option.yup02": "Da 02",
"sound.option.yup03": "Da 03",
"sound.option.yup04": "Da 04",
"sound.option.yup05": "Da 05",
"sound.option.yup06": "Da 06",
"settings.general.notifications.agent.title": "Agent",
"settings.general.notifications.agent.description":
"Prikaži sistemsku obavijest kada agent završi ili zahtijeva pažnju",
"settings.general.notifications.permissions.title": "Dozvole",
"settings.general.notifications.permissions.description": "Prikaži sistemsku obavijest kada je potrebna dozvola",
"settings.general.notifications.errors.title": "Greške",
"settings.general.notifications.errors.description": "Prikaži sistemsku obavijest kada dođe do greške",
"settings.general.sounds.agent.title": "Agent",
"settings.general.sounds.agent.description": "Pusti zvuk kada agent završi ili zahtijeva pažnju",
"settings.general.sounds.permissions.title": "Dozvole",
"settings.general.sounds.permissions.description": "Pusti zvuk kada je potrebna dozvola",
"settings.general.sounds.errors.title": "Greške",
"settings.general.sounds.errors.description": "Pusti zvuk kada dođe do greške",
"settings.shortcuts.title": "Prečice na tastaturi",
"settings.shortcuts.reset.button": "Vrati na podrazumijevano",
"settings.shortcuts.reset.toast.title": "Prečice resetovane",
"settings.shortcuts.reset.toast.description": "Prečice na tastaturi su vraćene na podrazumijevane.",
"settings.shortcuts.conflict.title": "Prečica je već u upotrebi",
"settings.shortcuts.conflict.description": "{{keybind}} je već dodijeljeno za {{titles}}.",
"settings.shortcuts.unassigned": "Nedodijeljeno",
"settings.shortcuts.pressKeys": "Pritisni tastere",
"settings.shortcuts.search.placeholder": "Pretraži prečice",
"settings.shortcuts.search.empty": "Nema pronađenih prečica",
"settings.shortcuts.group.general": "Opšte",
"settings.shortcuts.group.session": "Sesija",
"settings.shortcuts.group.navigation": "Navigacija",
"settings.shortcuts.group.modelAndAgent": "Model i agent",
"settings.shortcuts.group.terminal": "Terminal",
"settings.shortcuts.group.prompt": "Upit",
"settings.providers.title": "Provajderi",
"settings.providers.description": "Postavke provajdera će se ovdje moći podešavati.",
"settings.providers.section.connected": "Povezani provajderi",
"settings.providers.connected.empty": "Nema povezanih provajdera",
"settings.providers.section.popular": "Popularni provajderi",
"settings.providers.tag.environment": "Okruženje",
"settings.providers.tag.config": "Konfiguracija",
"settings.providers.tag.custom": "Prilagođeno",
"settings.providers.tag.other": "Ostalo",
"settings.models.title": "Modeli",
"settings.models.description": "Postavke modela će se ovdje moći podešavati.",
"settings.agents.title": "Agenti",
"settings.agents.description": "Postavke agenata će se ovdje moći podešavati.",
"settings.commands.title": "Komande",
"settings.commands.description": "Postavke komandi će se ovdje moći podešavati.",
"settings.mcp.title": "MCP",
"settings.mcp.description": "MCP postavke će se ovdje moći podešavati.",
"settings.permissions.title": "Dozvole",
"settings.permissions.description": "Kontroliši koje alate server smije koristiti po defaultu.",
"settings.permissions.section.tools": "Alati",
"settings.permissions.toast.updateFailed.title": "Neuspjelo ažuriranje dozvola",
"settings.permissions.action.allow": "Dozvoli",
"settings.permissions.action.ask": "Pitaj",
"settings.permissions.action.deny": "Zabrani",
"settings.permissions.tool.read.title": "Čitanje",
"settings.permissions.tool.read.description": "Čitanje datoteke (podudara se s putanjom datoteke)",
"settings.permissions.tool.edit.title": "Uređivanje",
"settings.permissions.tool.edit.description":
"Mijenjanje datoteka, uključujući izmjene, pisanja, patch-eve i multi-izmjene",
"settings.permissions.tool.glob.title": "Glob",
"settings.permissions.tool.glob.description": "Podudaranje datoteka pomoću glob šablona",
"settings.permissions.tool.grep.title": "Grep",
"settings.permissions.tool.grep.description": "Pretraživanje sadržaja datoteka pomoću regularnih izraza",
"settings.permissions.tool.list.title": "Lista",
"settings.permissions.tool.list.description": "Listanje datoteka unutar direktorija",
"settings.permissions.tool.bash.title": "Bash",
"settings.permissions.tool.bash.description": "Pokretanje shell komandi",
"settings.permissions.tool.task.title": "Zadatak",
"settings.permissions.tool.task.description": "Pokretanje pod-agenta",
"settings.permissions.tool.skill.title": "Vještina",
"settings.permissions.tool.skill.description": "Učitaj vještinu po nazivu",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Pokreni upite jezičnog servera",
"settings.permissions.tool.todoread.title": "Čitanje liste zadataka",
"settings.permissions.tool.todoread.description": "Čitanje liste zadataka",
"settings.permissions.tool.todowrite.title": "Ažuriranje liste zadataka",
"settings.permissions.tool.todowrite.description": "Ažuriraj listu zadataka",
"settings.permissions.tool.webfetch.title": "Web preuzimanje",
"settings.permissions.tool.webfetch.description": "Preuzmi sadržaj sa URL-a",
"settings.permissions.tool.websearch.title": "Web pretraga",
"settings.permissions.tool.websearch.description": "Pretražuj web",
"settings.permissions.tool.codesearch.title": "Pretraga koda",
"settings.permissions.tool.codesearch.description": "Pretraži kod na webu",
"settings.permissions.tool.external_directory.title": "Vanjski direktorij",
"settings.permissions.tool.external_directory.description": "Pristup datotekama izvan direktorija projekta",
"settings.permissions.tool.doom_loop.title": "Beskonačna petlja",
"settings.permissions.tool.doom_loop.description": "Otkriva ponovljene pozive alata sa identičnim unosom",
"session.delete.failed.title": "Neuspjelo brisanje sesije",
"session.delete.title": "Izbriši sesiju",
"session.delete.confirm": 'Izbriši sesiju "{{name}}"?',
"session.delete.button": "Izbriši sesiju",
"workspace.new": "Novi radni prostor",
"workspace.type.local": "lokalno",
"workspace.type.sandbox": "sandbox",
"workspace.create.failed.title": "Neuspješno kreiranje radnog prostora",
"workspace.delete.failed.title": "Neuspješno brisanje radnog prostora",
"workspace.resetting.title": "Resetovanje radnog prostora",
"workspace.resetting.description": "Ovo može potrajati minut.",
"workspace.reset.failed.title": "Neuspješno resetovanje radnog prostora",
"workspace.reset.success.title": "Radni prostor resetovan",
"workspace.reset.success.description": "Radni prostor sada odgovara podrazumijevanoj grani.",
"workspace.error.stillPreparing": "Radni prostor se još priprema",
"workspace.status.checking": "Provjera neobjedinjenih promjena...",
"workspace.status.error": "Nije moguće provjeriti git status.",
"workspace.status.clean": "Nisu pronađene neobjedinjene promjene.",
"workspace.status.dirty": "Pronađene su neobjedinjene promjene u ovom radnom prostoru.",
"workspace.delete.title": "Izbriši radni prostor",
"workspace.delete.confirm": 'Izbriši radni prostor "{{name}}"?',
"workspace.delete.button": "Izbriši radni prostor",
"workspace.reset.title": "Resetuj radni prostor",
"workspace.reset.confirm": 'Resetuj radni prostor "{{name}}"?',
"workspace.reset.button": "Resetuj radni prostor",
"workspace.reset.archived.none": "Nijedna aktivna sesija neće biti arhivirana.",
"workspace.reset.archived.one": "1 sesija će biti arhivirana.",
"workspace.reset.archived.many": "Biće arhivirano {{count}} sesija.",
"workspace.reset.note": "Ovo će resetovati radni prostor da odgovara podrazumijevanoj grani.",
}
+7 -18
View File
@@ -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",
@@ -322,22 +322,6 @@ export const dict = {
"context.usage.clickToView": "Klik for at se kontekst", "context.usage.clickToView": "Klik for at se kontekst",
"context.usage.view": "Se kontekstforbrug", "context.usage.view": "Se kontekstforbrug",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Sprog", "toast.language.title": "Sprog",
"toast.language.description": "Skiftede til {{language}}", "toast.language.description": "Skiftede til {{language}}",
@@ -349,6 +333,11 @@ export const dict = {
"toast.permissions.autoaccept.off.title": "Stoppede automatisk accept af ændringer", "toast.permissions.autoaccept.off.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",
+2 -18
View File
@@ -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",
@@ -328,22 +328,6 @@ export const dict = {
"context.usage.clickToView": "Klicken, um Kontext anzuzeigen", "context.usage.clickToView": "Klicken, um Kontext anzuzeigen",
"context.usage.view": "Kontextnutzung anzeigen", "context.usage.view": "Kontextnutzung anzeigen",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Sprache", "toast.language.title": "Sprache",
"toast.language.description": "Zu {{language}} gewechselt", "toast.language.description": "Zu {{language}} gewechselt",
+14 -2
View File
@@ -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",
@@ -342,6 +343,7 @@ export const dict = {
"language.ar": "العربية", "language.ar": "العربية",
"language.no": "Norsk", "language.no": "Norsk",
"language.br": "Português (Brasil)", "language.br": "Português (Brasil)",
"language.bs": "Bosanski",
"language.th": "ไทย", "language.th": "ไทย",
"toast.language.title": "Language", "toast.language.title": "Language",
@@ -350,6 +352,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 +470,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.openIn": "Open in",
"session.header.open.action": "Open {{app}}",
"session.header.open.ariaLabel": "Open in {{app}}",
"session.header.open.menu": "Open options",
"session.header.open.copyPath": "Copy Path",
"status.popover.trigger": "Status", "status.popover.trigger": "Status",
"status.popover.ariaLabel": "Server configurations", "status.popover.ariaLabel": "Server configurations",
+7 -18
View File
@@ -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",
@@ -323,22 +323,6 @@ export const dict = {
"context.usage.clickToView": "Haz clic para ver contexto", "context.usage.clickToView": "Haz clic para ver contexto",
"context.usage.view": "Ver uso del contexto", "context.usage.view": "Ver uso del contexto",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Idioma", "toast.language.title": "Idioma",
"toast.language.description": "Cambiado a {{language}}", "toast.language.description": "Cambiado a {{language}}",
@@ -350,6 +334,11 @@ export const dict = {
"toast.permissions.autoaccept.off.title": "Se dejó de aceptar ediciones automáticamente", "toast.permissions.autoaccept.off.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",
+7 -18
View File
@@ -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",
@@ -323,22 +323,6 @@ export const dict = {
"context.usage.clickToView": "Cliquez pour voir le contexte", "context.usage.clickToView": "Cliquez pour voir le contexte",
"context.usage.view": "Voir l'utilisation du contexte", "context.usage.view": "Voir l'utilisation du contexte",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Langue", "toast.language.title": "Langue",
"toast.language.description": "Passé à {{language}}", "toast.language.description": "Passé à {{language}}",
@@ -352,6 +336,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",
+2 -18
View File
@@ -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": "ファイル",
@@ -321,22 +321,6 @@ export const dict = {
"context.usage.clickToView": "クリックしてコンテキストを表示", "context.usage.clickToView": "クリックしてコンテキストを表示",
"context.usage.view": "コンテキスト使用量を表示", "context.usage.view": "コンテキスト使用量を表示",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "言語", "toast.language.title": "言語",
"toast.language.description": "{{language}}に切り替えました", "toast.language.description": "{{language}}に切り替えました",
+7 -18
View File
@@ -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": "파일",
@@ -324,22 +324,6 @@ export const dict = {
"context.usage.clickToView": "컨텍스트를 보려면 클릭", "context.usage.clickToView": "컨텍스트를 보려면 클릭",
"context.usage.view": "컨텍스트 사용량 보기", "context.usage.view": "컨텍스트 사용량 보기",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "언어", "toast.language.title": "언어",
"toast.language.description": "{{language}}(으)로 전환됨", "toast.language.description": "{{language}}(으)로 전환됨",
@@ -351,6 +335,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": "이 세션을 요약하려면 공급자를 연결하세요",
+7 -18
View File
@@ -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",
@@ -324,22 +324,6 @@ export const dict = {
"context.usage.clickToView": "Klikk for å se kontekst", "context.usage.clickToView": "Klikk for å se kontekst",
"context.usage.view": "Se kontekstforbruk", "context.usage.view": "Se kontekstforbruk",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Språk", "toast.language.title": "Språk",
"toast.language.description": "Byttet til {{language}}", "toast.language.description": "Byttet til {{language}}",
@@ -351,6 +335,11 @@ export const dict = {
"toast.permissions.autoaccept.off.title": "Sluttet å godta endringer automatisk", "toast.permissions.autoaccept.off.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",
+7 -18
View File
@@ -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",
@@ -322,22 +322,6 @@ export const dict = {
"context.usage.clickToView": "Kliknij, aby zobaczyć kontekst", "context.usage.clickToView": "Kliknij, aby zobaczyć kontekst",
"context.usage.view": "Pokaż użycie kontekstu", "context.usage.view": "Pokaż użycie kontekstu",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Język", "toast.language.title": "Język",
"toast.language.description": "Przełączono na {{language}}", "toast.language.description": "Przełączono na {{language}}",
@@ -349,6 +333,11 @@ export const dict = {
"toast.permissions.autoaccept.off.title": "Zatrzymano automatyczne akceptowanie edycji", "toast.permissions.autoaccept.off.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ę",
+7 -18
View File
@@ -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": "Файлы",
@@ -323,22 +323,6 @@ export const dict = {
"context.usage.clickToView": "Нажмите для просмотра контекста", "context.usage.clickToView": "Нажмите для просмотра контекста",
"context.usage.view": "Показать использование контекста", "context.usage.view": "Показать использование контекста",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Язык", "toast.language.title": "Язык",
"toast.language.description": "Переключено на {{language}}", "toast.language.description": "Переключено на {{language}}",
@@ -350,6 +334,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": "Подключите провайдера для суммаризации сессии",
+8 -19
View File
@@ -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": "ไฟล์",
@@ -326,22 +326,6 @@ export const dict = {
"context.usage.clickToView": "คลิกเพื่อดูบริบท", "context.usage.clickToView": "คลิกเพื่อดูบริบท",
"context.usage.view": "ดูการใช้บริบท", "context.usage.view": "ดูการใช้บริบท",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "ภาษา", "toast.language.title": "ภาษา",
"toast.language.description": "สลับไปที่ {{language}}", "toast.language.description": "สลับไปที่ {{language}}",
@@ -349,10 +333,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": "เชื่อมต่อผู้ให้บริการเพื่อสรุปเซสชันนี้",
+8 -19
View File
@@ -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": "文件",
@@ -324,27 +324,16 @@ export const dict = {
"context.usage.clickToView": "点击查看上下文", "context.usage.clickToView": "点击查看上下文",
"context.usage.view": "查看上下文用量", "context.usage.view": "查看上下文用量",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "语言", "toast.language.title": "语言",
"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": "编辑和写入权限将自动获批",
+8 -19
View File
@@ -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": "檔案",
@@ -321,27 +321,16 @@ export const dict = {
"context.usage.clickToView": "點擊查看上下文", "context.usage.clickToView": "點擊查看上下文",
"context.usage.view": "檢視上下文用量", "context.usage.view": "檢視上下文用量",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "語言", "toast.language.title": "語言",
"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": "編輯和寫入權限將自動獲准",
+1 -1
View File
@@ -31,7 +31,7 @@ export default function Layout(props: ParentProps) {
}) })
return ( return (
<Show when={directory()}> <Show when={directory()}>
<SDKProvider directory={directory()}> <SDKProvider directory={directory}>
<SyncProvider> <SyncProvider>
{iife(() => { {iife(() => {
const sync = useSync() const sync = useSync()
+335 -317
View File
@@ -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"
@@ -57,6 +58,7 @@ import { usePermission } from "@/context/permission"
import { Binary } from "@opencode-ai/util/binary" import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry" import { retry } from "@opencode-ai/util/retry"
import { playSound, soundSrc } from "@/utils/sound" import { playSound, soundSrc } from "@/utils/sound"
import { createAim } from "@/utils/aim"
import { Worktree as WorktreeState } from "@/utils/worktree" import { Worktree as WorktreeState } from "@/utils/worktree"
import { agentColor } from "@/utils/agent" import { agentColor } from "@/utils/agent"
@@ -108,7 +110,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 +121,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,
@@ -145,9 +147,20 @@ export default function Layout(props: ParentProps) {
const navLeave = { current: undefined as number | undefined } const navLeave = { current: undefined as number | undefined }
const aim = createAim({
enabled: () => !layout.sidebar.opened(),
active: () => state.hoverProject,
el: () => state.nav,
onActivate: (directory) => {
globalSync.child(directory)
setState("hoverProject", directory)
setState("hoverSession", undefined)
},
})
onCleanup(() => { onCleanup(() => {
if (navLeave.current === undefined) return if (navLeave.current !== undefined) clearTimeout(navLeave.current)
clearTimeout(navLeave.current) aim.reset()
}) })
const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined) const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined)
@@ -161,15 +174,22 @@ export default function Layout(props: ParentProps) {
createEffect(() => { createEffect(() => {
if (!layout.sidebar.opened()) return if (!layout.sidebar.opened()) return
aim.reset()
setState("hoverProject", undefined) setState("hoverProject", undefined)
}) })
createEffect(() => {
if (state.hoverProject !== undefined) return
aim.reset()
})
createEffect( createEffect(
on( on(
() => ({ dir: params.dir, id: params.id }), () => ({ dir: params.dir, id: params.id }),
() => { () => {
if (layout.sidebar.opened()) return if (layout.sidebar.opened()) return
if (!state.hoverProject) return if (!state.hoverProject) return
aim.reset()
setState("hoverSession", undefined) setState("hoverSession", undefined)
setState("hoverProject", undefined) setState("hoverProject", undefined)
}, },
@@ -179,13 +199,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 +526,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 +593,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 +773,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 +784,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" }))
} }
}) })
}) })
@@ -958,69 +1019,6 @@ export default function Layout(props: ParentProps) {
} }
} }
async function deleteSession(session: Session) {
const [store, setStore] = globalSync.child(session.directory)
const sessions = (store.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
const index = sessions.findIndex((s) => s.id === session.id)
const nextSession = sessions[index + 1] ?? sessions[index - 1]
const result = await globalSDK.client.session
.delete({ directory: session.directory, sessionID: session.id })
.then((x) => x.data)
.catch((err) => {
showToast({
title: language.t("session.delete.failed.title"),
description: errorMessage(err),
})
return false
})
if (!result) return
setStore(
produce((draft) => {
const removed = new Set<string>([session.id])
const byParent = new Map<string, string[]>()
for (const item of draft.session) {
const parentID = item.parentID
if (!parentID) continue
const existing = byParent.get(parentID)
if (existing) {
existing.push(item.id)
continue
}
byParent.set(parentID, [item.id])
}
const stack = [session.id]
while (stack.length) {
const parentID = stack.pop()
if (!parentID) continue
const children = byParent.get(parentID)
if (!children) continue
for (const child of children) {
if (removed.has(child)) continue
removed.add(child)
stack.push(child)
}
}
draft.session = draft.session.filter((s) => !removed.has(s.id))
}),
)
if (session.id === params.id) {
if (nextSession) {
navigate(`/${params.dir}/session/${nextSession.id}`)
} else {
navigate(`/${params.dir}/session`)
}
}
}
command.register(() => { command.register(() => {
const commands: CommandOption[] = [ const commands: CommandOption[] = [
{ {
@@ -1095,6 +1093,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"),
@@ -1251,15 +1272,6 @@ export default function Layout(props: ParentProps) {
globalSync.project.meta(project.worktree, { name }) globalSync.project.meta(project.worktree, { name })
} }
async function renameSession(session: Session, next: string) {
if (next === session.title) return
await globalSDK.client.session.update({
directory: session.directory,
sessionID: session.id,
title: next,
})
}
const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => { const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory) const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
if (current === next) return if (current === next) return
@@ -1410,33 +1422,6 @@ export default function Layout(props: ParentProps) {
}) })
} }
function DialogDeleteSession(props: { session: Session }) {
const handleDelete = async () => {
await deleteSession(props.session)
dialog.close()
}
return (
<Dialog title={language.t("session.delete.title")} fit>
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
<div class="flex flex-col gap-1">
<span class="text-14-regular text-text-strong">
{language.t("session.delete.confirm", { name: props.session.title })}
</span>
</div>
<div class="flex justify-end gap-2">
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</Button>
<Button variant="primary" size="large" onClick={handleDelete}>
{language.t("session.delete.button")}
</Button>
</div>
</div>
</Dialog>
)
}
function DialogDeleteWorkspace(props: { root: string; directory: string }) { function DialogDeleteWorkspace(props: { root: string; directory: string }) {
const name = createMemo(() => getFilename(props.directory)) const name = createMemo(() => getFilename(props.directory))
const [data, setData] = createStore({ const [data, setData] = createStore({
@@ -1790,10 +1775,6 @@ export default function Layout(props: ParentProps) {
const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded()) const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded())
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
const isActive = createMemo(() => props.session.id === params.id) const isActive = createMemo(() => props.session.id === params.id)
const [menu, setMenu] = createStore({
open: false,
pendingRename: false,
})
const hoverPrefetch = { current: undefined as ReturnType<typeof setTimeout> | undefined } const hoverPrefetch = { current: undefined as ReturnType<typeof setTimeout> | undefined }
const cancelHoverPrefetch = () => { const cancelHoverPrefetch = () => {
@@ -1820,7 +1801,7 @@ export default function Layout(props: ParentProps) {
const item = ( const item = (
<A <A
href={`${props.slug}/session/${props.session.id}`} href={`${props.slug}/session/${props.session.id}`}
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${menu.open ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`} class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
onPointerEnter={scheduleHoverPrefetch} onPointerEnter={scheduleHoverPrefetch}
onPointerLeave={cancelHoverPrefetch} onPointerLeave={cancelHoverPrefetch}
onMouseEnter={scheduleHoverPrefetch} onMouseEnter={scheduleHoverPrefetch}
@@ -1852,14 +1833,9 @@ export default function Layout(props: ParentProps) {
</Match> </Match>
</Switch> </Switch>
</div> </div>
<InlineEditor <span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
id={`session:${props.session.id}`} {props.session.title}
value={() => props.session.title} </span>
onSave={(next) => renameSession(props.session, next)}
class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
stopPropagation
/>
<Show when={props.session.summary}> <Show when={props.session.summary}>
{(summary) => ( {(summary) => (
<div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden"> <div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
@@ -1907,7 +1883,10 @@ export default function Layout(props: ParentProps) {
getLabel={messageLabel} getLabel={messageLabel}
onMessageSelect={(message) => { onMessageSelect={(message) => {
if (!isActive()) { if (!isActive()) {
sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`) layout.pendingMessage.set(
`${base64Encode(props.session.directory)}/${props.session.id}`,
message.id,
)
navigate(`${props.slug}/session/${props.session.id}`) navigate(`${props.slug}/session/${props.session.id}`)
return return
} }
@@ -1924,49 +1903,25 @@ export default function Layout(props: ParentProps) {
<div <div
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`} class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
classList={{ classList={{
"opacity-100 pointer-events-auto": menu.open, "opacity-100 pointer-events-auto": !!props.mobile,
"opacity-0 pointer-events-none": !menu.open, "opacity-0 pointer-events-none": !props.mobile,
"group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true, "group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
"group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true, "group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
}} }}
> >
<DropdownMenu modal={!sidebarHovering()} open={menu.open} onOpenChange={(open) => setMenu("open", open)}> <Tooltip value={language.t("common.archive")} placement="top">
<Tooltip value={language.t("common.moreOptions")} placement="top"> <IconButton
<DropdownMenu.Trigger icon="archive"
as={IconButton} variant="ghost"
icon="dot-grid" class="size-6 rounded-md"
variant="ghost" aria-label={language.t("common.archive")}
class="size-6 rounded-md data-[expanded]:bg-surface-base-active" onClick={(event) => {
aria-label={language.t("common.moreOptions")} event.preventDefault()
/> event.stopPropagation()
</Tooltip> void archiveSession(props.session)
<DropdownMenu.Portal mount={!props.mobile ? state.nav : undefined}> }}
<DropdownMenu.Content />
onCloseAutoFocus={(event) => { </Tooltip>
if (!menu.pendingRename) return
event.preventDefault()
setMenu("pendingRename", false)
openEditor(`session:${props.session.id}`, props.session.title)
}}
>
<DropdownMenu.Item
onSelect={() => {
setMenu("pendingRename", true)
setMenu("open", false)
}}
>
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => archiveSession(props.session)}>
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogDeleteSession session={props.session} />)}>
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div> </div>
</div> </div>
) )
@@ -2100,9 +2055,10 @@ export default function Layout(props: ParentProps) {
const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local()) const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local())
const boot = createMemo(() => open() || active()) const boot = createMemo(() => open() || active())
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false) const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
const loading = createMemo(() => open() && !booted() && sessions().length === 0)
const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length) const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
const busy = createMemo(() => isBusy(props.directory)) const busy = createMemo(() => isBusy(props.directory))
const wasBusy = createMemo((prev) => prev || busy(), false)
const loading = createMemo(() => open() && !booted() && sessions().length === 0 && !wasBusy())
const loadMore = async () => { const loadMore = async () => {
setWorkspaceStore("limit", (limit) => limit + 5) setWorkspaceStore("limit", (limit) => limit + 5)
await globalSync.project.loadSessions(props.directory) await globalSync.project.loadSessions(props.directory)
@@ -2310,10 +2266,13 @@ export default function Layout(props: ParentProps) {
() => props.project.vcs === "git" && layout.sidebar.workspaces(props.project.worktree)(), () => 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 +2310,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={(event: MouseEvent) => {
if (!overlay()) return
aim.enter(props.project.worktree, event)
}}
onMouseLeave={() => {
if (!overlay()) return
aim.leave(props.project.worktree)
}}
onFocus={() => {
if (!overlay()) return
aim.activate(props.project.worktree)
}}
onClick={() => navigateToProject(props.project.worktree)}
onBlur={() => setOpen(false)}
>
<ProjectIcon project={props.project} notify />
</ContextMenu.Trigger>
<ContextMenu.Portal mount={!props.mobile ? state.nav : undefined}>
<ContextMenu.Content>
<ContextMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={props.project} />)}>
<ContextMenu.ItemLabel>{language.t("common.edit")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
<ContextMenu.Item
data-action="project-workspaces-toggle"
data-project={base64Encode(props.project.worktree)}
disabled={props.project.vcs !== "git" && !layout.sidebar.workspaces(props.project.worktree)()}
onSelect={() => {
const enabled = layout.sidebar.workspaces(props.project.worktree)()
if (enabled) {
layout.sidebar.toggleWorkspaces(props.project.worktree)
return
}
if (props.project.vcs !== "git") return
layout.sidebar.toggleWorkspaces(props.project.worktree)
}}
>
<ContextMenu.ItemLabel>
{layout.sidebar.workspaces(props.project.worktree)()
? language.t("sidebar.workspaces.disable")
: language.t("sidebar.workspaces.enable")}
</ContextMenu.ItemLabel>
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item
data-action="project-close-menu"
data-project={base64Encode(props.project.worktree)}
onSelect={() => closeProject(props.project.worktree)}
>
<ContextMenu.ItemLabel>{language.t("common.close")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
) )
return ( 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)
}} }}
@@ -2485,17 +2489,22 @@ export default function Layout(props: ParentProps) {
} }
const LocalWorkspace = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => { const LocalWorkspace = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.project.worktree) const workspace = createMemo(() => {
const [store, setStore] = globalSync.child(props.project.worktree)
return { store, setStore }
})
const slug = createMemo(() => base64Encode(props.project.worktree)) const slug = createMemo(() => base64Encode(props.project.worktree))
const sessions = createMemo(() => const sessions = createMemo(() => {
workspaceStore.session const store = workspace().store
.filter((session) => session.directory === workspaceStore.path.directory) return store.session
.filter((session) => session.directory === store.path.directory)
.filter((session) => !session.parentID && !session.time?.archived) .filter((session) => !session.parentID && !session.time?.archived)
.toSorted(sortSessions(Date.now())), .toSorted(sortSessions(Date.now()))
) })
const children = createMemo(() => { const children = createMemo(() => {
const store = workspace().store
const map = new Map<string, string[]>() const map = new Map<string, string[]>()
for (const session of workspaceStore.session) { for (const session of store.session) {
if (!session.parentID) continue if (!session.parentID) continue
const existing = map.get(session.parentID) const existing = map.get(session.parentID)
if (existing) { if (existing) {
@@ -2506,11 +2515,11 @@ export default function Layout(props: ParentProps) {
} }
return map return map
}) })
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false) const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
const loading = createMemo(() => !booted() && sessions().length === 0) const loading = createMemo(() => !booted() && sessions().length === 0)
const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length) const hasMore = createMemo(() => workspace().store.sessionTotal > sessions().length)
const loadMore = async () => { const loadMore = async () => {
setWorkspaceStore("limit", (limit) => limit + 5) workspace().setStore("limit", (limit) => limit + 5)
await globalSync.project.loadSessions(props.project.worktree) await globalSync.project.loadSessions(props.project.worktree)
} }
@@ -2566,6 +2575,8 @@ export default function Layout(props: ParentProps) {
if (!created?.directory) return if (!created?.directory) return
setWorkspaceName(created.directory, created.branch, project.id, created.branch)
const local = project.worktree const local = project.worktree
const key = workspaceKey(created.directory) const key = workspaceKey(created.directory)
const root = workspaceKey(local) const root = workspaceKey(local)
@@ -2615,7 +2626,7 @@ export default function Layout(props: ParentProps) {
}} }}
style={{ width: panelProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }} 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 +2635,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 +2644,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 +2652,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 +2663,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 +2695,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 +2706,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>
) )
} }
@@ -2816,7 +2833,7 @@ export default function Layout(props: ParentProps) {
return ( return (
<div class="flex h-full w-full overflow-hidden"> <div class="flex h-full w-full overflow-hidden">
<div class="w-16 shrink-0 bg-background-base flex flex-col items-center overflow-hidden"> <div class="w-16 shrink-0 bg-background-base flex flex-col items-center overflow-hidden" onMouseMove={aim.move}>
<div class="flex-1 min-h-0 w-full"> <div class="flex-1 min-h-0 w-full">
<DragDropProvider <DragDropProvider
onDragStart={handleDragStart} onDragStart={handleDragStart}
@@ -2911,6 +2928,7 @@ export default function Layout(props: ParentProps) {
navLeave.current = undefined navLeave.current = undefined
}} }}
onMouseLeave={() => { onMouseLeave={() => {
aim.reset()
if (!sidebarHovering()) return if (!sidebarHovering()) return
if (navLeave.current !== undefined) clearTimeout(navLeave.current) if (navLeave.current !== undefined) clearTimeout(navLeave.current)
@@ -2926,7 +2944,7 @@ export default function Layout(props: ParentProps) {
</div> </div>
<Show when={!layout.sidebar.opened() ? hoverProjectData() : undefined} keyed> <Show when={!layout.sidebar.opened() ? hoverProjectData() : undefined} keyed>
{(project) => ( {(project) => (
<div class="absolute inset-y-0 left-16 z-50 flex"> <div class="absolute inset-y-0 left-16 z-50 flex" onMouseEnter={aim.reset}>
<SidebarPanel project={project} /> <SidebarPanel project={project} />
</div> </div>
)} )}
File diff suppressed because it is too large Load Diff
+138
View File
@@ -0,0 +1,138 @@
type Point = { x: number; y: number }
export function createAim(props: {
enabled: () => boolean
active: () => string | undefined
el: () => HTMLElement | undefined
onActivate: (id: string) => void
delay?: number
max?: number
tolerance?: number
edge?: number
}) {
const state = {
locs: [] as Point[],
timer: undefined as number | undefined,
pending: undefined as string | undefined,
over: undefined as string | undefined,
last: undefined as Point | undefined,
}
const delay = props.delay ?? 250
const max = props.max ?? 4
const tolerance = props.tolerance ?? 80
const edge = props.edge ?? 18
const cancel = () => {
if (state.timer !== undefined) clearTimeout(state.timer)
state.timer = undefined
state.pending = undefined
}
const reset = () => {
cancel()
state.over = undefined
state.last = undefined
state.locs.length = 0
}
const move = (event: MouseEvent) => {
if (!props.enabled()) return
const el = props.el()
if (!el) return
const rect = el.getBoundingClientRect()
const x = event.clientX
const y = event.clientY
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) return
state.locs.push({ x, y })
if (state.locs.length > max) state.locs.shift()
}
const wait = () => {
if (!props.enabled()) return 0
if (!props.active()) return 0
const el = props.el()
if (!el) return 0
if (state.locs.length < 2) return 0
const rect = el.getBoundingClientRect()
const loc = state.locs[state.locs.length - 1]
if (!loc) return 0
const prev = state.locs[0] ?? loc
if (prev.x < rect.left || prev.x > rect.right || prev.y < rect.top || prev.y > rect.bottom) return 0
if (state.last && loc.x === state.last.x && loc.y === state.last.y) return 0
if (rect.right - loc.x <= edge) {
state.last = loc
return delay
}
const upper = { x: rect.right, y: rect.top - tolerance }
const lower = { x: rect.right, y: rect.bottom + tolerance }
const slope = (a: Point, b: Point) => (b.y - a.y) / (b.x - a.x)
const decreasing = slope(loc, upper)
const increasing = slope(loc, lower)
const prevDecreasing = slope(prev, upper)
const prevIncreasing = slope(prev, lower)
if (decreasing < prevDecreasing && increasing > prevIncreasing) {
state.last = loc
return delay
}
state.last = undefined
return 0
}
const activate = (id: string) => {
cancel()
props.onActivate(id)
}
const request = (id: string) => {
if (!id) return
if (props.active() === id) return
if (!props.active()) {
activate(id)
return
}
const ms = wait()
if (ms === 0) {
activate(id)
return
}
cancel()
state.pending = id
state.timer = window.setTimeout(() => {
state.timer = undefined
if (state.pending !== id) return
state.pending = undefined
if (!props.enabled()) return
if (!props.active()) return
if (state.over !== id) return
props.onActivate(id)
}, ms)
}
const enter = (id: string, event: MouseEvent) => {
if (!props.enabled()) return
state.over = id
move(event)
request(id)
}
const leave = (id: string) => {
if (state.over === id) state.over = undefined
if (state.pending === id) cancel()
}
return { move, enter, leave, activate, request, cancel, reset }
}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.1.48", "version": "1.1.52",
"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()
@@ -26,6 +26,7 @@ export const oaCompatHelper: ProviderHelper = () => ({
modifyUrl: (providerApi: string) => providerApi + "/chat/completions", modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => { modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
headers.set("authorization", `Bearer ${apiKey}`) headers.set("authorization", `Bearer ${apiKey}`)
headers.set("x-session-affinity", headers.get("x-opencode-session") ?? "")
}, },
modifyBody: (body: Record<string, any>) => { modifyBody: (body: Record<string, any>) => {
return { return {
@@ -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.`)
}, },
} }
} }
+4 -4
View File
@@ -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.52",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@@ -25,9 +25,9 @@
"db": "sst shell drizzle-kit", "db": "sst shell drizzle-kit",
"db-dev": "sst shell --stage dev -- drizzle-kit", "db-dev": "sst shell --stage dev -- drizzle-kit",
"db-prod": "sst shell --stage production -- drizzle-kit", "db-prod": "sst shell --stage production -- drizzle-kit",
"shell": "sst shell -- bun", "shell": "sst shell",
"shell-dev": "sst shell --stage dev -- bun", "shell-dev": "sst shell --stage dev",
"shell-prod": "sst shell --stage production -- bun", "shell-prod": "sst shell --stage production",
"update-models": "script/update-models.ts", "update-models": "script/update-models.ts",
"promote-models-to-dev": "script/promote-models.ts dev", "promote-models-to-dev": "script/promote-models.ts dev",
"promote-models-to-prod": "script/promote-models.ts production", "promote-models-to-prod": "script/promote-models.ts production",
@@ -17,10 +17,8 @@ const oldValues = Array.from({ length: PARTS }, (_, i) => {
?.split("=") ?.split("=")
.slice(1) .slice(1)
.join("=") .join("=")
// TODO if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
//if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`) return value
//return value
return value ?? ""
}) })
// store the prettified json to a temp file // store the prettified json to a temp file
+2
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.1.48", "version": "1.1.52",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.1.48", "version": "1.1.52",
"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",
+1 -1
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"private": true, "private": true,
"version": "1.1.48", "version": "1.1.52",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@@ -6,6 +6,19 @@
"permissions": [ "permissions": [
"core:default", "core:default",
"opener:default", "opener:default",
{
"identifier": "opener:allow-open-path",
"allow": [
{ "path": "**/*" },
{ "path": "/**/*" },
{ "path": "**/.*/*/**" },
{ "path": "/**/.*/*/**" },
{ "path": "**/*", "app": true },
{ "path": "/**/*", "app": true },
{ "path": "**/.*/*/**", "app": true },
{ "path": "/**/.*/*/**", "app": true }
]
},
"deep-link:default", "deep-link:default",
"core:window:allow-start-dragging", "core:window:allow-start-dragging",
"core:window:allow-set-theme", "core:window:allow-set-theme",
@@ -42,6 +42,13 @@
"active": true, "active": true,
"targets": ["deb", "rpm", "dmg", "nsis", "app"], "targets": ["deb", "rpm", "dmg", "nsis", "app"],
"externalBin": ["sidecars/opencode-cli"], "externalBin": ["sidecars/opencode-cli"],
"linux": {
"rpm": {
"compression": {
"type": "none"
}
}
},
"macOS": { "macOS": {
"entitlements": "./entitlements.plist" "entitlements": "./entitlements.plist"
}, },
@@ -21,6 +21,11 @@
"files": { "files": {
"/usr/share/metainfo/ai.opencode.opencode.metainfo.xml": "release/appstream.metainfo.xml" "/usr/share/metainfo/ai.opencode.opencode.metainfo.xml": "release/appstream.metainfo.xml"
} }
},
"rpm": {
"compression": {
"type": "none"
}
} }
} }
}, },
+32
View File
@@ -0,0 +1,32 @@
export const dict = {
"desktop.menu.checkForUpdates": "Provjeri ažuriranja...",
"desktop.menu.installCli": "Instaliraj CLI...",
"desktop.menu.reloadWebview": "Ponovo učitavanje webview-a",
"desktop.menu.restart": "Restartuj",
"desktop.dialog.chooseFolder": "Odaberi folder",
"desktop.dialog.chooseFile": "Odaberi datoteku",
"desktop.dialog.saveFile": "Sačuvaj datoteku",
"desktop.updater.checkFailed.title": "Provjera ažuriranja nije uspjela",
"desktop.updater.checkFailed.message": "Nije moguće provjeriti ažuriranja",
"desktop.updater.none.title": "Nema dostupnog ažuriranja",
"desktop.updater.none.message": "Već koristiš najnoviju verziju OpenCode-a",
"desktop.updater.downloadFailed.title": "Ažuriranje nije uspjelo",
"desktop.updater.downloadFailed.message": "Neuspjelo preuzimanje ažuriranja",
"desktop.updater.downloaded.title": "Ažuriranje preuzeto",
"desktop.updater.downloaded.prompt":
"Verzija {{version}} OpenCode-a je preuzeta. Želiš li da je instaliraš i ponovo pokreneš aplikaciju?",
"desktop.updater.installFailed.title": "Ažuriranje nije uspjelo",
"desktop.updater.installFailed.message": "Neuspjela instalacija ažuriranja",
"desktop.cli.installed.title": "CLI instaliran",
"desktop.cli.installed.message":
"CLI je instaliran u {{path}}\n\nRestartuj terminal da bi koristio komandu 'opencode'.",
"desktop.cli.failed.title": "Instalacija nije uspjela",
"desktop.cli.failed.message": "Neuspjela instalacija CLI-a: {{error}}",
"desktop.error.serverStartFailed.title": "OpenCode se nije mogao pokrenuti",
"desktop.error.serverStartFailed.description":
"Lokalni OpenCode server se nije mogao pokrenuti. Restartuj aplikaciju ili provjeri mrežne postavke (VPN/proxy) i pokušaj ponovo.",
}
+37 -2
View File
@@ -15,6 +15,7 @@ import { dict as desktopRu } from "./ru"
import { dict as desktopAr } from "./ar" import { dict as desktopAr } from "./ar"
import { dict as desktopNo } from "./no" import { dict as desktopNo } from "./no"
import { dict as desktopBr } from "./br" import { dict as desktopBr } from "./br"
import { dict as desktopBs } from "./bs"
import { dict as appEn } from "../../../app/src/i18n/en" import { dict as appEn } from "../../../app/src/i18n/en"
import { dict as appZh } from "../../../app/src/i18n/zh" import { dict as appZh } from "../../../app/src/i18n/zh"
@@ -30,13 +31,45 @@ import { dict as appRu } from "../../../app/src/i18n/ru"
import { dict as appAr } from "../../../app/src/i18n/ar" import { dict as appAr } from "../../../app/src/i18n/ar"
import { dict as appNo } from "../../../app/src/i18n/no" import { dict as appNo } from "../../../app/src/i18n/no"
import { dict as appBr } from "../../../app/src/i18n/br" import { dict as appBr } from "../../../app/src/i18n/br"
import { dict as appBs } from "../../../app/src/i18n/bs"
export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br" export type Locale =
| "en"
| "zh"
| "zht"
| "ko"
| "de"
| "es"
| "fr"
| "da"
| "ja"
| "pl"
| "ru"
| "ar"
| "no"
| "br"
| "bs"
type RawDictionary = typeof appEn & typeof desktopEn type RawDictionary = typeof appEn & typeof desktopEn
type Dictionary = i18n.Flatten<RawDictionary> type Dictionary = i18n.Flatten<RawDictionary>
const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"] const LOCALES: readonly Locale[] = [
"en",
"zh",
"zht",
"ko",
"de",
"es",
"fr",
"da",
"ja",
"pl",
"ru",
"bs",
"ar",
"no",
"br",
]
function detectLocale(): Locale { function detectLocale(): Locale {
if (typeof navigator !== "object") return "en" if (typeof navigator !== "object") return "en"
@@ -64,6 +97,7 @@ function detectLocale(): Locale {
) )
return "no" return "no"
if (language.toLowerCase().startsWith("pt")) return "br" if (language.toLowerCase().startsWith("pt")) return "br"
if (language.toLowerCase().startsWith("bs")) return "bs"
} }
return "en" return "en"
@@ -108,6 +142,7 @@ function build(locale: Locale): Dictionary {
if (locale === "ar") return { ...base, ...i18n.flatten(appAr), ...i18n.flatten(desktopAr) } if (locale === "ar") return { ...base, ...i18n.flatten(appAr), ...i18n.flatten(desktopAr) }
if (locale === "no") return { ...base, ...i18n.flatten(appNo), ...i18n.flatten(desktopNo) } if (locale === "no") return { ...base, ...i18n.flatten(appNo), ...i18n.flatten(desktopNo) }
if (locale === "br") return { ...base, ...i18n.flatten(appBr), ...i18n.flatten(desktopBr) } if (locale === "br") return { ...base, ...i18n.flatten(appBr), ...i18n.flatten(desktopBr) }
if (locale === "bs") return { ...base, ...i18n.flatten(appBs), ...i18n.flatten(desktopBs) }
return { ...base, ...i18n.flatten(appKo), ...i18n.flatten(desktopKo) } return { ...base, ...i18n.flatten(appKo), ...i18n.flatten(desktopKo) }
} }
+5 -11
View File
@@ -4,6 +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 { openPath as openerOpenPath } from "@tauri-apps/plugin-opener"
import { open as shellOpen } from "@tauri-apps/plugin-shell" import { open as shellOpen } from "@tauri-apps/plugin-shell"
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"
@@ -30,17 +31,6 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
void initI18n() void initI18n()
// Floating UI can call getComputedStyle with non-elements (e.g., null refs, virtual elements).
// This happens on all platforms (WebView2 on Windows, WKWebView on macOS), not just Windows.
const originalGetComputedStyle = window.getComputedStyle
window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => {
if (!(elt instanceof Element)) {
// Fall back to a safe element when a non-element is passed.
return originalGetComputedStyle(document.documentElement, pseudoElt ?? undefined)
}
return originalGetComputedStyle(elt, pseudoElt ?? undefined)
}) as typeof window.getComputedStyle
let update: Update | null = null let update: Update | null = null
const deepLinkEvent = "opencode:deep-link" const deepLinkEvent = "opencode:deep-link"
@@ -98,6 +88,10 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
void shellOpen(url).catch(() => undefined) void shellOpen(url).catch(() => undefined)
}, },
openPath(path: string, app?: string) {
return openerOpenPath(path, app)
},
back() { back() {
window.history.back() window.history.back()
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.1.48", "version": "1.1.52",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+6 -6
View File
@@ -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.52"
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.52/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.52/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.52/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.52/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.52/opencode-windows-x64.zip"
cmd = "./opencode.exe" cmd = "./opencode.exe"
args = ["acp"] args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.1.48", "version": "1.1.52",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+8 -5
View File
@@ -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.52",
"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"
}, },
@@ -49,7 +50,7 @@
"dependencies": { "dependencies": {
"@actions/core": "1.11.1", "@actions/core": "1.11.1",
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.13.0", "@agentclientprotocol/sdk": "0.14.1",
"@ai-sdk/amazon-bedrock": "3.0.74", "@ai-sdk/amazon-bedrock": "3.0.74",
"@ai-sdk/anthropic": "2.0.58", "@ai-sdk/anthropic": "2.0.58",
"@ai-sdk/azure": "2.0.91", "@ai-sdk/azure": "2.0.91",
@@ -68,9 +69,10 @@
"@ai-sdk/provider-utils": "3.0.20", "@ai-sdk/provider-utils": "3.0.20",
"@ai-sdk/togetherai": "1.0.34", "@ai-sdk/togetherai": "1.0.34",
"@ai-sdk/vercel": "1.0.33", "@ai-sdk/vercel": "1.0.33",
"@ai-sdk/xai": "2.0.56", "@ai-sdk/xai": "2.0.51",
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@gitlab/gitlab-ai-provider": "3.3.1", "@gitlab/gitlab-ai-provider": "3.4.0",
"@gitlab/opencode-gitlab-auth": "1.3.2",
"@hono/standard-validator": "0.1.5", "@hono/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 +93,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",
+3 -67
View File
@@ -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,
@@ -93,73 +95,7 @@ if (!Script.preview) {
"", "",
].join("\n") ].join("\n")
// Source-based PKGBUILD for opencode for (const [pkg, pkgbuild] of [["opencode-bin", binaryPkgbuild]]) {
const sourcePkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode'",
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
"url='https://github.com/anomalyco/opencode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode-bin')",
"depends=('ripgrep')",
"makedepends=('git' 'bun' 'go')",
"",
`source=("opencode-\${pkgver}.tar.gz::https://github.com/anomalyco/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
`sha256sums=('SKIP')`,
"",
"build() {",
` cd "opencode-\${pkgver}"`,
` bun install`,
" cd ./packages/opencode",
` OPENCODE_CHANNEL=latest OPENCODE_VERSION=${pkgver} bun run ./script/build.ts --single`,
"}",
"",
"package() {",
` cd "opencode-\${pkgver}/packages/opencode"`,
' mkdir -p "${pkgdir}/usr/bin"',
' target_arch="x64"',
' case "$CARCH" in',
' x86_64) target_arch="x64" ;;',
' aarch64) target_arch="arm64" ;;',
' *) printf "unsupported architecture: %s\\n" "$CARCH" >&2 ; return 1 ;;',
" esac",
' libc=""',
" if command -v ldd >/dev/null 2>&1; then",
" if ldd --version 2>&1 | grep -qi musl; then",
' libc="-musl"',
" fi",
" fi",
' if [ -z "$libc" ] && ls /lib/ld-musl-* >/dev/null 2>&1; then',
' libc="-musl"',
" fi",
' base=""',
' if [ "$target_arch" = "x64" ]; then',
" if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then",
' base="-baseline"',
" fi",
" fi",
' bin="dist/opencode-linux-${target_arch}${base}${libc}/bin/opencode"',
' if [ ! -f "$bin" ]; then',
' printf "unable to find binary for %s%s%s\\n" "$target_arch" "$base" "$libc" >&2',
" return 1",
" fi",
' install -Dm755 "$bin" "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
for (const [pkg, pkgbuild] of [
["opencode-bin", binaryPkgbuild],
["opencode", sourcePkgbuild],
]) {
for (let i = 0; i < 30; i++) { for (let i = 0; i < 30; i++) {
try { try {
await $`rm -rf ./dist/aur-${pkg}` await $`rm -rf ./dist/aur-${pkg}`
+118 -11
View File
@@ -25,6 +25,7 @@ import {
type SetSessionModeResponse, type SetSessionModeResponse,
type ToolCallContent, type ToolCallContent,
type ToolKind, type ToolKind,
type Usage,
} from "@agentclientprotocol/sdk" } from "@agentclientprotocol/sdk"
import { Log } from "../util/log" import { Log } from "../util/log"
@@ -38,7 +39,7 @@ import { Config } from "@/config/config"
import { Todo } from "@/session/todo" import { Todo } from "@/session/todo"
import { z } from "zod" import { z } from "zod"
import { LoadAPIKeyError } from "ai" import { LoadAPIKeyError } from "ai"
import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
import { applyPatch } from "diff" import { applyPatch } from "diff"
type ModeOption = { id: string; name: string; description?: string } type ModeOption = { id: string; name: string; description?: string }
@@ -49,6 +50,74 @@ const DEFAULT_VARIANT_VALUE = "default"
export namespace ACP { export namespace ACP {
const log = Log.create({ service: "acp-agent" }) const log = Log.create({ service: "acp-agent" })
async function getContextLimit(
sdk: OpencodeClient,
providerID: string,
modelID: string,
directory: string,
): Promise<number | null> {
const providers = await sdk.config
.providers({ directory })
.then((x) => x.data?.providers ?? [])
.catch((error) => {
log.error("failed to get providers for context limit", { error })
return []
})
const provider = providers.find((p) => p.id === providerID)
const model = provider?.models[modelID]
return model?.limit.context ?? null
}
async function sendUsageUpdate(
connection: AgentSideConnection,
sdk: OpencodeClient,
sessionID: string,
directory: string,
): Promise<void> {
const messages = await sdk.session
.messages({ sessionID, directory }, { throwOnError: true })
.then((x) => x.data)
.catch((error) => {
log.error("failed to fetch messages for usage update", { error })
return undefined
})
if (!messages) return
const assistantMessages = messages.filter(
(m): m is { info: AssistantMessage; parts: SessionMessageResponse["parts"] } => m.info.role === "assistant",
)
const lastAssistant = assistantMessages[assistantMessages.length - 1]
if (!lastAssistant) return
const msg = lastAssistant.info
const size = await getContextLimit(sdk, msg.providerID, msg.modelID, directory)
if (!size) {
// Cannot calculate usage without known context size
return
}
const used = msg.tokens.input + (msg.tokens.cache?.read ?? 0)
const totalCost = assistantMessages.reduce((sum, m) => sum + m.info.cost, 0)
await connection
.sessionUpdate({
sessionId: sessionID,
update: {
sessionUpdate: "usage_update",
used,
size,
cost: { amount: totalCost, currency: "USD" },
},
})
.catch((error) => {
log.error("failed to send usage update", { error })
})
}
export async function init({ sdk: _sdk }: { sdk: OpencodeClient }) { export async function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
return { return {
create: (connection: AgentSideConnection, fullConfig: ACPConfig) => { create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
@@ -546,6 +615,8 @@ export namespace ACP {
await this.processMessage(msg) await this.processMessage(msg)
} }
await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
return result return result
} catch (e) { } catch (e) {
const error = MessageV2.fromError(e, { const error = MessageV2.fromError(e, {
@@ -654,6 +725,8 @@ export namespace ACP {
await this.processMessage(msg) await this.processMessage(msg)
} }
await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
return mode return mode
} catch (e) { } catch (e) {
const error = MessageV2.fromError(e, { const error = MessageV2.fromError(e, {
@@ -677,11 +750,15 @@ export namespace ACP {
log.info("resume_session", { sessionId, mcpServers: mcpServers.length }) log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
return this.loadSessionMode({ const result = await this.loadSessionMode({
cwd: directory, cwd: directory,
mcpServers, mcpServers,
sessionId, sessionId,
}) })
await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
return result
} catch (e) { } catch (e) {
const error = MessageV2.fromError(e, { const error = MessageV2.fromError(e, {
providerID: this.config.defaultModel?.providerID ?? "unknown", providerID: this.config.defaultModel?.providerID ?? "unknown",
@@ -1239,13 +1316,22 @@ export namespace ACP {
return { name, args: rest.join(" ").trim() } return { name, args: rest.join(" ").trim() }
})() })()
const done = { const buildUsage = (msg: AssistantMessage): Usage => ({
stopReason: "end_turn" as const, totalTokens:
_meta: {}, msg.tokens.input +
} msg.tokens.output +
msg.tokens.reasoning +
(msg.tokens.cache?.read ?? 0) +
(msg.tokens.cache?.write ?? 0),
inputTokens: msg.tokens.input,
outputTokens: msg.tokens.output,
thoughtTokens: msg.tokens.reasoning || undefined,
cachedReadTokens: msg.tokens.cache?.read || undefined,
cachedWriteTokens: msg.tokens.cache?.write || undefined,
})
if (!cmd) { if (!cmd) {
await this.sdk.session.prompt({ const response = await this.sdk.session.prompt({
sessionID, sessionID,
model: { model: {
providerID: model.providerID, providerID: model.providerID,
@@ -1256,14 +1342,22 @@ export namespace ACP {
agent, agent,
directory, directory,
}) })
return done const msg = response.data?.info
await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
return {
stopReason: "end_turn" as const,
usage: msg ? buildUsage(msg) : undefined,
_meta: {},
}
} }
const command = await this.config.sdk.command const command = await this.config.sdk.command
.list({ directory }, { throwOnError: true }) .list({ directory }, { throwOnError: true })
.then((x) => x.data!.find((c) => c.name === cmd.name)) .then((x) => x.data!.find((c) => c.name === cmd.name))
if (command) { if (command) {
await this.sdk.session.command({ const response = await this.sdk.session.command({
sessionID, sessionID,
command: command.name, command: command.name,
arguments: cmd.args, arguments: cmd.args,
@@ -1271,7 +1365,15 @@ export namespace ACP {
agent, agent,
directory, directory,
}) })
return done const msg = response.data?.info
await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
return {
stopReason: "end_turn" as const,
usage: msg ? buildUsage(msg) : undefined,
_meta: {},
}
} }
switch (cmd.name) { switch (cmd.name) {
@@ -1288,7 +1390,12 @@ export namespace ACP {
break break
} }
return done await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
return {
stopReason: "end_turn" as const,
_meta: {},
}
} }
async cancel(params: CancelNotification) { async cancel(params: CancelNotification) {
+6 -5
View File
@@ -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" } }),
) )
} }
+13 -10
View File
@@ -5,12 +5,12 @@ 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"
import { proxied } from "@/util/proxied"
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,14 +75,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]
const proxied = !!( if (!modExists || !cachedVersion) {
process.env.HTTP_PROXY || // continue to install
process.env.HTTPS_PROXY || } else if (version !== "latest" && cachedVersion === version) {
process.env.http_proxy || return mod
process.env.https_proxy } else if (version === "latest") {
) const isOutdated = await PackageRegistry.isOutdated(pkg, cachedVersion, Global.Path.cache)
if (!isOutdated) return mod
log.info("Cached version is outdated, proceeding with install", { pkg, cachedVersion })
}
// Build command arguments // Build command arguments
const args = [ const args = [
@@ -90,7 +93,7 @@ export namespace BunProc {
"--force", "--force",
"--exact", "--exact",
// TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936) // TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
...(proxied ? ["--no-cache"] : []), ...(proxied() ? ["--no-cache"] : []),
"--cwd", "--cwd",
Global.Path.cache, Global.Path.cache,
pkg + "@" + version, pkg + "@" + version,
+48
View File
@@ -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
}
}
+370 -181
View File
@@ -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],
@@ -546,6 +546,7 @@ export function Session() {
{ {
title: showThinking() ? "Hide thinking" : "Show thinking", title: showThinking() ? "Hide thinking" : "Show thinking",
value: "session.toggle.thinking", value: "session.toggle.thinking",
keybind: "display_thinking",
category: "Session", category: "Session",
slash: { slash: {
name: "thinking", name: "thinking",
@@ -1624,6 +1625,7 @@ function BlockTool(props: {
function Bash(props: ToolProps<typeof BashTool>) { function Bash(props: ToolProps<typeof BashTool>) {
const { theme } = useTheme() const { theme } = useTheme()
const sync = useSync() const sync = useSync()
const isRunning = createMemo(() => props.part.state.status === "running")
const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? "")) const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? ""))
const [expanded, setExpanded] = createSignal(false) const [expanded, setExpanded] = createSignal(false)
const lines = createMemo(() => output().split("\n")) const lines = createMemo(() => output().split("\n"))
@@ -1664,6 +1666,7 @@ function Bash(props: ToolProps<typeof BashTool>) {
<BlockTool <BlockTool
title={title()} title={title()}
part={props.part} part={props.part}
spinner={isRunning()}
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined} onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
> >
<box gap={1}> <box gap={1}>
@@ -1,5 +1,5 @@
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { createMemo, For, Show } from "solid-js" import { createMemo, createSignal, For, Show } from "solid-js"
import { useKeyboard } from "@opentui/solid" import { useKeyboard } from "@opentui/solid"
import type { TextareaRenderable } from "@opentui/core" import type { TextareaRenderable } from "@opentui/core"
import { useKeybind } from "../../context/keybind" import { useKeybind } from "../../context/keybind"
@@ -19,6 +19,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
const questions = createMemo(() => props.request.questions) const questions = createMemo(() => props.request.questions)
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true) const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
const tabs = createMemo(() => (single() ? 1 : questions().length + 1)) // questions + confirm tab (no confirm for single select) const tabs = createMemo(() => (single() ? 1 : questions().length + 1)) // questions + confirm tab (no confirm for single select)
const [tabHover, setTabHover] = createSignal<number | "confirm" | null>(null)
const [store, setStore] = createStore({ const [store, setStore] = createStore({
tab: 0, tab: 0,
answers: [] as QuestionAnswer[], answers: [] as QuestionAnswer[],
@@ -269,7 +270,15 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
<box <box
paddingLeft={1} paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={isActive() ? theme.accent : theme.backgroundElement} backgroundColor={
isActive()
? theme.accent
: tabHover() === index()
? theme.backgroundElement
: theme.backgroundPanel
}
onMouseOver={() => setTabHover(index())}
onMouseOut={() => setTabHover(null)}
onMouseUp={() => selectTab(index())} onMouseUp={() => selectTab(index())}
> >
<text <text
@@ -290,7 +299,11 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
<box <box
paddingLeft={1} paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={confirm() ? theme.accent : theme.backgroundElement} backgroundColor={
confirm() ? theme.accent : tabHover() === "confirm" ? theme.backgroundElement : theme.backgroundPanel
}
onMouseOver={() => setTabHover("confirm")}
onMouseOut={() => setTabHover(null)}
onMouseUp={() => selectTab(questions().length)} onMouseUp={() => selectTab(questions().length)}
> >
<text fg={confirm() ? selectedForeground(theme, theme.accent) : theme.textMuted}>Confirm</text> <text fg={confirm() ? selectedForeground(theme, theme.accent) : theme.textMuted}>Confirm</text>
@@ -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)
} }
} }
+1 -1
View File
@@ -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}`,
) )
} }
+8 -1
View File
@@ -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 }
} }
+121 -26
View File
@@ -24,10 +24,13 @@ import { LSPServer } from "../lsp/server"
import { BunProc } from "@/bun" import { BunProc } from "@/bun"
import { Installation } from "@/installation" import { Installation } from "@/installation"
import { ConfigMarkdown } from "./markdown" import { ConfigMarkdown } from "./markdown"
import { existsSync } from "fs" import { constants, 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"
import { proxied } from "@/util/proxied"
import { iife } from "@/util/iife"
export namespace Config { export namespace Config {
const log = Log.create({ service: "config" }) const log = Log.create({ service: "config" })
@@ -142,9 +145,20 @@ export namespace Config {
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR }) log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
} }
const deps: Promise<void>[] = []
for (const dir of unique(directories)) { for (const dir of unique(directories)) {
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) { const files = ["opencode.jsonc", "opencode.json"]
for (const file of ["opencode.jsonc", "opencode.json"]) { const isConfigDir = dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR || dir === Global.Path.config
const configs = isConfigDir
? await Promise.all(files.map((file) => loadFile(path.join(dir, file), { resolvePlugins: false })))
: []
const plugins = configs.flatMap((config) => config.plugin ?? [])
const shouldInstall = await needsInstall(dir, plugins)
if (shouldInstall) await installDependencies(dir, plugins)
if (isConfigDir) {
for (const file of files) {
log.debug(`loading config from ${path.join(dir, file)}`) log.debug(`loading config from ${path.join(dir, file)}`)
result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file))) result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file)))
// to satisfy the type checker // to satisfy the type checker
@@ -154,10 +168,6 @@ export namespace Config {
} }
} }
const exists = existsSync(path.join(dir, "node_modules"))
const installing = installDependencies(dir)
if (!exists) await installing
result.command = mergeDeep(result.command ?? {}, await loadCommand(dir)) result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
result.agent = mergeDeep(result.agent, await loadAgent(dir)) result.agent = mergeDeep(result.agent, await loadAgent(dir))
result.agent = mergeDeep(result.agent, await loadMode(dir)) result.agent = mergeDeep(result.agent, await loadMode(dir))
@@ -230,30 +240,111 @@ export namespace Config {
return { return {
config: result, config: result,
directories, directories,
deps,
} }
}) })
export async function installDependencies(dir: string) { export async function waitForDependencies() {
const pkg = path.join(dir, "package.json") const deps = await state().then((x) => x.deps)
await Promise.all(deps)
}
if (!(await Bun.file(pkg).exists())) { export async function installDependencies(dir: string, plugins: string[] = []) {
await Bun.write(pkg, "{}") const pkg = path.join(dir, "package.json")
const targetVersion = Installation.isLocal() ? "*" : Installation.VERSION
const json = await Bun.file(pkg)
.json()
.catch(() => ({}))
json.dependencies = {
...json.dependencies,
"@opencode-ai/plugin": targetVersion,
} }
const pluginDeps = deps(plugins)
for (const [name, version] of Object.entries(pluginDeps)) {
json.dependencies[name] = version
}
await Bun.write(pkg, JSON.stringify(json, null, 2))
await new Promise((resolve) => setTimeout(resolve, 3000))
const gitignore = path.join(dir, ".gitignore") const gitignore = path.join(dir, ".gitignore")
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(
["add", "@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION), "--exact"],
{
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",
// TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
...(proxied() ? ["--no-cache"] : []),
],
{ cwd: dir },
).catch(() => {})
}
async function isWritable(dir: string) {
try {
await fs.access(dir, constants.W_OK)
return true
} catch {
return false
}
}
async function needsInstall(dir: string, plugins: string[] = []) {
// Some config dirs may be read-only.
// Installing deps there will fail; skip installation in that case.
const writable = await isWritable(dir)
if (!writable) {
log.debug("config dir is not writable, skipping dependency install", { dir })
return false
}
const nodeModules = path.join(dir, "node_modules")
if (!existsSync(nodeModules)) return true
const pkg = path.join(dir, "package.json")
const pkgFile = Bun.file(pkg)
const pkgExists = await pkgFile.exists()
if (!pkgExists) return true
const parsed = await pkgFile.json().catch(() => null)
const dependencies = parsed?.dependencies ?? {}
const depVersion = dependencies["@opencode-ai/plugin"]
if (!depVersion) return true
const targetVersion = Installation.isLocal() ? "latest" : Installation.VERSION
if (targetVersion === "latest") {
const isOutdated = await PackageRegistry.isOutdated("@opencode-ai/plugin", depVersion, dir)
if (!isOutdated) return false
log.info("Cached version is outdated, proceeding with install", {
pkg: "@opencode-ai/plugin",
cachedVersion: depVersion,
})
return true
}
if (depVersion !== targetVersion) return true
const pluginDeps = deps(plugins)
for (const [name, version] of Object.entries(pluginDeps)) {
if (dependencies[name] !== version) return true
}
return false
}
function deps(items: string[]) {
const result: Record<string, string> = {}
for (const item of items) {
if (!item) continue
if (item.startsWith("file://")) continue
if (item.startsWith("./") || item.startsWith("../") || item.startsWith("/") || item.startsWith("~")) continue
const lastAt = item.lastIndexOf("@")
const pkg = lastAt > 0 ? item.substring(0, lastAt) : item
const version = lastAt > 0 ? item.substring(lastAt + 1) : "latest"
result[pkg] = version
}
return result
} }
function rel(item: string, patterns: string[]) { function rel(item: string, patterns: string[]) {
@@ -617,10 +708,12 @@ export namespace Config {
.describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"), .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()
@@ -835,6 +928,7 @@ export namespace Config {
terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"), terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"), terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"), tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"),
}) })
.strict() .strict()
.meta({ .meta({
@@ -860,6 +954,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()
@@ -1146,7 +1241,7 @@ export namespace Config {
return result return result
}) })
async function loadFile(filepath: string): Promise<Info> { async function loadFile(filepath: string, options: { resolvePlugins?: boolean } = {}): Promise<Info> {
log.info("loading", { path: filepath }) log.info("loading", { path: filepath })
let text = await Bun.file(filepath) let text = await Bun.file(filepath)
.text() .text()
@@ -1155,10 +1250,10 @@ export namespace Config {
throw new JsonError({ path: filepath }, { cause: err }) throw new JsonError({ path: filepath }, { cause: err })
}) })
if (!text) return {} if (!text) return {}
return load(text, filepath) return load(text, filepath, options)
} }
async function load(text: string, configFilepath: string) { async function load(text: string, configFilepath: string, options: { resolvePlugins?: boolean } = {}) {
const original = text const original = text
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => { text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || "" return process.env[varName] || ""
@@ -1234,7 +1329,7 @@ export namespace Config {
await Bun.write(configFilepath, updated).catch(() => {}) await Bun.write(configFilepath, updated).catch(() => {})
} }
const data = parsed.data const data = parsed.data
if (data.plugin) { if (data.plugin && options.resolvePlugins !== false) {
for (let i = 0; i < data.plugin.length; i++) { for (let i = 0; i < data.plugin.length; i++) {
const plugin = data.plugin[i] const plugin = data.plugin[i]
try { try {

Some files were not shown because too many files have changed in this diff Show More