Compare commits

...

349 Commits

Author SHA1 Message Date
adamelmore
98ed41332c chore: cleanup 2026-01-26 06:00:31 -06:00
David Hill
0f26e19d38 wip: new release modal
- highlight key updates or new features
- needs some transition love
- all copy including text and video placeholder
2026-01-26 06:00:31 -06:00
David Hill
6c6e81884f fix: search clear icon 2026-01-26 06:00:31 -06:00
Aiden Cline
022265829e ignore: update AGENTS.md to state that inference should be used 2026-01-26 00:04:00 -05:00
Ryan Vogel
23d85f4739 docs: add warning about Claude Pro/Max subscription support (#10595) 2026-01-25 22:28:01 -05:00
Ryan Vogel
8c4bf225f2 fix(web): update spacing on the changelog page 2026-01-25 21:41:12 -05:00
adamelmore
99ae3a7717 chore: cleanup 2026-01-25 20:40:00 -06:00
adamelmore
9d35a0bcb6 chore: cleanup 2026-01-25 20:40:00 -06:00
adamelmore
a09a8701ae chore: cleanup 2026-01-25 20:40:00 -06:00
adamelmore
00d960d080 chore: cleanup 2026-01-25 20:40:00 -06:00
adamelmore
5993a098b4 fix(core): don't override source in custom provider loaders 2026-01-25 20:40:00 -06:00
adamelmore
c323d96deb wip(app): provider settings 2026-01-25 20:40:00 -06:00
adamelmore
03d884797c wip(app): provider settings 2026-01-25 20:40:00 -06:00
adamelmore
a5b72a7d99 fix(ui): tab click hit area 2026-01-25 20:32:59 -06:00
Ryan Vogel
cc0085676b Add collapsible sections, sticky version header, and style refinements for changelog highlights 2026-01-25 21:26:56 -05:00
Aiden Cline
eaad75b176 tweak: adjust tui syncing logic to help prevent case where agents would be undefined / missing 2026-01-25 21:26:47 -05:00
Ryan Vogel
ab3268896d Add highlight tag parsing for changelog with video support 2026-01-25 21:26:43 -05:00
adamelmore
3d23d2df71 fix(app): missing translations for status 2026-01-25 19:39:09 -06:00
Aiden Cline
578361de64 fix: remove broken app.tsx command option 2026-01-25 20:27:41 -05:00
adamelmore
5369e96ab7 fix(app): line selection colors 2026-01-25 19:07:24 -06:00
adamelmore
fbcf138526 chore: better i18n links 2026-01-25 19:07:23 -06:00
Ariane Emory
3071720ce7 fix(tui): Move animations toggle to global System category (resolves #10495) (#10497) 2026-01-25 17:57:47 -05:00
Aiden Cline
f0830a74bb ignore: update AGENTS.md 2026-01-25 17:54:17 -05:00
Frank
57532326f7 zen: handle subscription payment failure 2026-01-25 17:46:18 -05:00
MartinWie
045c30acf3 docs: fix permission event name (permission.asked not permission.updated) (#10588) 2026-01-25 17:44:57 -05:00
Aiden Cline
94dd0a8dbe ignore: rm spoof and bump plugin version 2026-01-25 17:27:24 -05:00
adamelmore
a84843507f chore: readme links 2026-01-25 15:53:50 -06:00
adamelmore
835b396591 chore: i18n for readme 2026-01-25 15:46:06 -06:00
adamelmore
407f34fed5 chore: cleanup 2026-01-25 13:20:18 -06:00
adamelmore
94ce289dd9 fix(app): run start command after reset 2026-01-25 13:20:18 -06:00
adamelmore
d115f33b59 fix(app): don't allow workspaces in non-vcs projects 2026-01-25 13:15:35 -06:00
adamelmore
14b00f64a7 fix(app): escape should always close dialogs 2026-01-25 12:17:35 -06:00
GitHub Action
fc57c074ae chore: generate 2026-01-25 17:45:07 +00:00
Aiden Cline
e49306b86c rm log statement 2026-01-25 12:44:17 -05:00
opencode
056186225b release: v1.1.36 2026-01-25 17:29:26 +00:00
GitHub Action
b982ab2fbc chore: generate 2026-01-25 17:26:11 +00:00
adamelmore
9a89cd91d7 fix(app): line selection styling 2026-01-25 11:25:31 -06:00
adamelmore
65ac318282 fix(app): user message fade 2026-01-25 11:25:31 -06:00
Ryan Vogel
e491f5cc16 fix(web): add & fix the download button (#10566) 2026-01-25 12:20:39 -05:00
ishaksebsib
ebe86e40a0 fix(tui): prevent crash when theme search returns no results (#10565) 2026-01-25 12:11:49 -05:00
Aiden Cline
9407a6fd7c ignore: rm STYLE_GUIDE, consolidate in AGENTS.md 2026-01-25 11:56:21 -05:00
GitHub Action
d75dca29e9 chore: generate 2026-01-25 16:49:03 +00:00
adamelmore
471fc06f01 chore(app): visual cleanup 2026-01-25 10:48:20 -06:00
adamelmore
4c2d597ae6 fix(app): line selection colors 2026-01-25 10:30:10 -06:00
adamelmore
2b07291e17 fix(app): scroll to comment on click 2026-01-25 10:30:10 -06:00
GitHub Action
d25120680d chore: generate 2026-01-25 14:07:33 +00:00
Devin Griffin
a900c89245 fix(app): mobile horizontal scrolling due to session stat btn (#10487) 2026-01-25 08:06:56 -06:00
Rahul A Mistry
caecc7911d fix(app): cursor on resize (#10293) 2026-01-25 08:04:25 -06:00
adamelmore
f7a4cdcd32 fix(app): no default model crash 2026-01-25 07:01:36 -06:00
adamelmore
e9152b174f fix(app): comment line placement in diffs 2026-01-25 06:50:27 -06:00
adamelmore
dcc8d1a638 perf(app): performance improvements 2026-01-25 06:43:27 -06:00
adamelmore
ddc4e89359 fix(app): cleanup comment component usage 2026-01-25 06:20:50 -06:00
GitHub Action
a5c058e584 ignore: update download stats 2026-01-25 2026-01-25 12:05:15 +00:00
Michael Yochpaz
0bc4a43320 fix(provider): enable thinking for google-vertex-anthropic models (#10442)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-01-24 23:42:29 -05:00
Aiden Cline
e2d0d85d93 test: fix 2026-01-24 23:42:12 -05:00
Aiden Cline
2917a2fa61 test: fix 2026-01-24 23:41:47 -05:00
GitHub Action
12473561ba chore: generate 2026-01-25 04:12:09 +00:00
Aiden Cline
397ee419d1 tweak: make question valdiation more lax to avoid tool call failures 2026-01-24 23:11:21 -05:00
Github Action
b1072053ba chore: update nix node_modules hashes 2026-01-25 04:02:46 +00:00
GitHub Action
a64f8d1b11 chore: generate 2026-01-25 04:01:25 +00:00
Aiden Cline
7f55a9736d chore: bump hey api 2026-01-24 23:00:40 -05:00
GitHub Action
460513a835 chore: generate 2026-01-25 00:37:59 +00:00
opencode
33298e8775 release: v1.1.35 2026-01-25 00:37:58 +00:00
adamelmore
2f9f588f77 fix(app): submit button state 2026-01-24 18:34:42 -06:00
Github Action
d6efb797b5 chore: update nix node_modules hashes 2026-01-25 00:33:10 +00:00
Rahul A Mistry
399fec770f fix(app): markdown rendering with morphdom for better dom functions (#10373) 2026-01-24 18:29:58 -06:00
adamelmore
8d1a66d043 fix(app): unnecessary suspense flash 2026-01-24 17:17:44 -06:00
GitHub Action
e1fe86e6d7 chore: generate 2026-01-24 23:10:03 +00:00
David Hill
93e948ae12 fix(ui): ensure comment popover appears above other comment icons 2026-01-24 23:09:22 +00:00
David Hill
8714b1a3ac add active state to comment cards in prompt input 2026-01-24 23:09:22 +00:00
David Hill
4ded06f05d update: theme variables 2026-01-24 23:09:22 +00:00
GitHub Action
6d0fecb985 chore: generate 2026-01-24 23:02:21 +00:00
adamelmore
e223d1a0e5 fix: type error 2026-01-24 17:01:37 -06:00
adamelmore
3fdd6ec120 fix(app): terminal clone needs remount 2026-01-24 16:58:43 -06:00
adamelmore
2f1be914cd fix(app): remove terminal connection error overlay 2026-01-24 16:58:43 -06:00
GitHub Action
1269766cb8 chore: generate 2026-01-24 22:58:15 +00:00
adamelmore
ae11cad13b fix: type error 2026-01-24 16:57:38 -06:00
adamelmore
10d227b8d6 fix(ui): tab focus state 2026-01-24 16:57:38 -06:00
adamelmore
d97cd56867 fix(ui): popover exit ux 2026-01-24 16:57:38 -06:00
David Hill
43906f56c8 fix(app): remove space between ellipsis and truncated text in comment card tooltip 2026-01-24 22:42:52 +00:00
David Hill
241087d1dc fix(app): update status popover empty state text color and centering 2026-01-24 22:35:24 +00:00
David Hill
fba77a364c fix(ui): prevent tooltip fade when forceOpen is true 2026-01-24 22:29:38 +00:00
David Hill
3d956c5f7e fix(ui): show 'Copied' tooltip instantly when copy button clicked 2026-01-24 22:25:20 +00:00
David Hill
4a4c1b31a7 fix(ui): update copy button tooltip gutter and label to 'Copy link' 2026-01-24 22:22:01 +00:00
David Hill
30111d2b16 fix(ui): prevent focus on share popover text field 2026-01-24 22:17:59 +00:00
David Hill
b695216063 fix(ui): align list search input width with list items 2026-01-24 22:11:31 +00:00
David Hill
c2ec608212 feat(ui): add link icon and use it for copy-to-clipboard buttons
Replace copy icon with new link icon in share URL copy button
and TextField copyable button for better visual indication.
2026-01-24 22:02:09 +00:00
David Hill
c1af7ddc6b fix(app): adjust share popover position 64px to the left 2026-01-24 22:02:09 +00:00
David Hill
cf7c6417f8 fix(app): update share popover gutter to 6px and radius to match status dropdown 2026-01-24 22:02:09 +00:00
David Hill
937474aff0 fix(app): add 8px spacing between share button and icon buttons in titlebar 2026-01-24 22:02:09 +00:00
David Hill
a878b8d7ac refactor(app): replace Popover with DropdownMenu for server options 2026-01-24 22:02:09 +00:00
David Hill
b824fc5516 fix(app): update options icon button styling - active state and hover 2026-01-24 22:02:09 +00:00
David Hill
c56f6127c7 fix(app): change server item actions div padding from px-4 to pl-4 2026-01-24 22:02:09 +00:00
David Hill
8845f2b926 feat(ui): add onFilter callback to List, discard add server row when searching 2026-01-24 22:02:09 +00:00
David Hill
df4d839577 fix(app): position status circle inside input wrapper and fix dialog padding 2026-01-24 22:02:09 +00:00
David Hill
8fe42cd5dc fix(app): remove hover background color from server list items 2026-01-24 22:02:09 +00:00
David Hill
a169c2987b fix(app): allow add server row to grow for error message 2026-01-24 22:02:08 +00:00
David Hill
a5c08bc4f8 fix(app): update add server button and row styling 2026-01-24 22:02:08 +00:00
David Hill
02aea77e92 feat(app): update manage servers dialog styling and behavior 2026-01-24 22:02:08 +00:00
David Hill
a98add29d1 feat(app): add truncation tooltip to server items in status popover 2026-01-24 22:02:08 +00:00
David Hill
d01df32e36 fix(app): update server and MCP item styles in status popover 2026-01-24 22:02:08 +00:00
David Hill
2c620e1742 fix(app): update status popover styling and positioning 2026-01-24 22:02:08 +00:00
David Hill
262084d7e6 fix(app): use rounded-sm for explicit 4px border radius 2026-01-24 22:02:08 +00:00
David Hill
b089358503 fix(app): update titlebar spacing and status popover styling 2026-01-24 22:02:08 +00:00
David Hill
02456376ce fix(app): enable submit button when comment cards are present 2026-01-24 22:02:08 +00:00
GitHub Action
faf2609bc5 chore: generate 2026-01-24 21:53:51 +00:00
Liyang Zhu
aeeb05e4a0 feat(app): back button in subagent sessions (#10439) 2026-01-24 15:53:15 -06:00
adamelmore
847a7ca009 fix(app): don't show scroll to bottom if no scroll 2026-01-24 15:01:17 -06:00
adamelmore
dc1ff0e63e fix(app): model select not closing on escape 2026-01-24 15:01:05 -06:00
adamelmore
7ba25c6afb fix(app): model selector ux 2026-01-24 15:01:05 -06:00
adamelmore
b951187a6e fix(app): no select on new session 2026-01-24 14:58:16 -06:00
Maharshi Patel
8f99e9a606 fix(opentui): question selection click when terminal unfocused (#9731) 2026-01-24 15:12:16 -05:00
adamelmore
27b45d070d fix(app): scrolling for unpaid model selector 2026-01-24 13:38:14 -06:00
Dax Raad
07d7dc083c ci: remove unused environment variables from test workflow
Removes MODELS_DEV_API_JSON and OPENCODE_DISABLE_MODELS_FETCH environment variables that were redundant in the test workflow, simplifying the configuration.
2026-01-24 14:26:59 -05:00
Dax Raad
eaa622e852 fix adam 2026-01-24 14:25:01 -05:00
Dax Raad
ff9c186485 tests 2026-01-24 14:16:46 -05:00
adamelmore
41f2653a30 fix(app): prompt submission failing on first message 2026-01-24 13:15:34 -06:00
Dax Raad
0d9ca0ea31 sync 2026-01-24 14:14:17 -05:00
Dax Raad
68bd16df69 core: fix models snapshot loading to prevent caching issues 2026-01-24 14:06:07 -05:00
GitHub Action
b3901ac38b chore: generate 2026-01-24 18:59:51 +00:00
David Hill
48236ee0ef feat(ui): add critical shadow for comment input validation, set editor popover radius to 14px 2026-01-24 18:59:07 +00:00
David Hill
e2bffc29f2 fix(ui): change read-only comment popover border-radius to 8px 2026-01-24 18:59:07 +00:00
David Hill
fda897eac4 fix(app): improve comment popover - remove disabled state, add error styling, fix click-outside detection 2026-01-24 18:59:07 +00:00
David Hill
e5d2d984b6 feat(app): change prompt placeholder based on comment count 2026-01-24 18:59:07 +00:00
David Hill
bfb0885371 fix(util): change filename truncation to end truncation, add truncateMiddle utility 2026-01-24 18:59:07 +00:00
David Hill
0d41f1fc24 fix(ui): remove unnecessary !important from diff selection styles 2026-01-24 18:59:07 +00:00
David Hill
363ff153a4 fix(ui): fix selected line number color in diff view for light/dark mode 2026-01-24 18:59:07 +00:00
David Hill
f4bcf0062a fix(app): adjust prompt container padding to 16px bottom and horizontal 2026-01-24 18:59:07 +00:00
David Hill
9759afad83 fix(app): adjust prompt input positioning - 12px from bottom/right, remove session panel bottom padding 2026-01-24 18:59:07 +00:00
David Hill
ac204ed89d fix(ui): add escape/click-away to close read-only comment popovers, 10px radius, remove 'Click to view context' text 2026-01-24 18:59:06 +00:00
adamelmore
1080f37f9c fix(app): don't use findLast 2026-01-24 12:41:50 -06:00
adamelmore
d90b4c9ebd fix(app): line selection ux 2026-01-24 12:41:50 -06:00
adamelmore
42b802b688 fix(app): line selection ux fixes 2026-01-24 12:41:50 -06:00
Dax Raad
fa1a54ba3d fix nix 2026-01-24 13:24:40 -05:00
GitHub Action
8f0d08fae0 chore: generate 2026-01-24 18:17:29 +00:00
Joseph Campuzano
15801a01ba fix: add state to pause existing audio for demo menus, add support fo… (#10428) 2026-01-24 12:16:53 -06:00
Dax Raad
32e6bcae3b core: fix unicode filename handling in snapshot diff by disabling quote escaping
This ensures unicode and special characters in filenames are displayed correctly when generating diff patches, allowing proper file detection and revert operations
2026-01-24 13:07:07 -05:00
zerone0x
087d7da14d fix(provider): deep merge providerOptions in applyCaching (#10323)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-24 13:07:04 -05:00
GitHub Action
442a735883 chore: generate 2026-01-24 18:04:12 +00:00
OpeOginni
67ea21b55a feat(web): implement new server management for web and desktop (#8513) 2026-01-24 12:03:36 -06:00
Britt
f4cf3f4976 fix(web): construct apply_patch metadata before requesting permission (#10422) 2026-01-24 12:00:21 -06:00
Dax Raad
e3c1861a3e get rid of models.dev macro 2026-01-24 12:27:13 -05:00
GitHub Action
d9eebba90e chore: generate 2026-01-24 17:27:03 +00:00
Aiden Cline
c5ef6452b3 ignore: update commit.md 2026-01-24 12:26:21 -05:00
Sebastian Herrlinger
ad27427b48 use min/maxHeight for question textarea 2026-01-24 12:14:12 -05:00
Aiden Cline
88bcd04659 tweak: tell the model what model it is in environment section of prompt 2026-01-24 12:10:15 -05:00
justfortheloveof
077d17d433 fix: permission prompt should ignore keyboard events while dialog stack len > 0 (#10338) 2026-01-24 12:01:46 -05:00
Filip
6511243152 feat(docs): add a desktop troubleshooting guide (#10397) 2026-01-24 12:00:51 -05:00
GitHub Action
ea8d727e28 chore: generate 2026-01-24 16:55:54 +00:00
Frank
b590bda5ed zen: show reload error 2026-01-24 11:48:19 -05:00
Frank
d8bbb6df60 zen: disable reload when reload fails 2026-01-24 11:48:19 -05:00
adamelmore
7c2e59de68 fix(app): new workspace expanded and at the top 2026-01-24 09:12:32 -06:00
adamelmore
fa510161f6 fix(app): missing translations 2026-01-24 09:12:12 -06:00
adamelmore
6abe86806f fix(app): better error screen when connecting to sidecar 2026-01-24 09:10:02 -06:00
adamelmore
6d8e994383 fix(app): line selection fixes 2026-01-24 09:09:27 -06:00
GitHub Action
ae77ef3370 chore: generate 2026-01-24 14:47:36 +00:00
Ariane Emory
68e504bdc2 fix(tui): Use selectedForeground for question prompt tab text visibility (resolves #10334) (#10337) 2026-01-24 09:46:59 -05:00
Rahul A Mistry
91287dd7bc fix(app): tooltip text in light mode to use inverted neutral scale (#9786) 2026-01-24 07:17:20 -06:00
adamelmore
09f45320b7 chore: cleanup 2026-01-24 07:00:41 -06:00
adamelmore
962ab3bc8c fix(app): reactive loops 2026-01-24 07:00:41 -06:00
adamelmore
da8f3e92a7 perf(app): better session stream rendering 2026-01-24 07:00:41 -06:00
GitHub Action
04b511e1fe chore: generate 2026-01-24 12:50:47 +00:00
adamelmore
456469d541 fix(app): tool details indentation 2026-01-24 06:50:01 -06:00
adamelmore
d96877f173 fix(app): sticky header top 2026-01-24 06:50:01 -06:00
Ariane Emory
98b66ff933 feat(desktop): add Iosevka as a font choice (resolves #10103) (#10347) 2026-01-24 06:28:58 -06:00
Devin Griffin
5f7111fe93 fix(app): Always close hovercard when view-sessions clicked (#10326) 2026-01-24 06:27:32 -06:00
Devin Griffin
d5f78a7278 fix(app): Fix plan mode btn keyboard a11y issues (#10330) 2026-01-24 06:27:09 -06:00
GitHub Action
1533c50ac3 ignore: update download stats 2026-01-24 2026-01-24 12:04:28 +00:00
David Hill
0cc206a1a5 update: border radius on popover card 2026-01-24 06:18:56 +00:00
David Hill
d4443d79c7 update: border variable 2026-01-24 06:18:55 +00:00
David Hill
c9215e8dc3 fix(ui): style review tab comment button to match file tab - blue background, white comment icon 2026-01-24 06:18:55 +00:00
David Hill
58788192f4 fix(ui): close comment input popover on Escape key or click away 2026-01-24 06:18:55 +00:00
David Hill
40ab6ac862 fix(ui): use shadow-lg-border-base on read-only comment popovers and align label spacing 2026-01-24 06:18:55 +00:00
David Hill
31f80a45af fix(ui): remove border from comment input popover 2026-01-24 06:18:55 +00:00
David Hill
0a9f51f87f fix(ui): position read-only comment popover below icon with 4px gutter 2026-01-24 06:18:55 +00:00
David Hill
af6bd9d3b1 fix(ui): style comment popovers - 14px radius, move label below, use text-weak for label, text-strong 14px for comment 2026-01-24 06:18:55 +00:00
David Hill
c66da17364 fix(ui): move filename and line count below comment text in popovers 2026-01-24 06:18:55 +00:00
David Hill
b280207481 fix(app): add tooltip with path, 6px spacing before close icon, and reduce filename truncation to 14 chars 2026-01-24 06:18:55 +00:00
David Hill
75cccc305a feat(app): add middle truncation for filename in comment card 2026-01-24 06:18:55 +00:00
David Hill
18ea09868a fix(app): truncate filename from start to show end of path 2026-01-24 06:18:55 +00:00
David Hill
1df697dec7 fix(app): remove gap between filename and comment in comment card 2026-01-24 06:18:55 +00:00
David Hill
1476c4ca49 fix(app): add shadow-xs-border with hover state to comment card 2026-01-24 06:18:55 +00:00
David Hill
3b3ab29d8c fix(app): comment card styling - 48px height, 2px gap, truncate filename while keeping line count visible 2026-01-24 06:18:55 +00:00
David Hill
258d207fd6 fix(app): increase comment font size to 12px 2026-01-24 06:18:55 +00:00
David Hill
4b64bff11b fix(app): add 8px gap before close icon and truncate long filenames 2026-01-24 06:18:55 +00:00
David Hill
c70e8b5880 fix(app): keep close icon in top right of comment card 2026-01-24 06:18:55 +00:00
David Hill
42a1a1202c fix(app): add transition-all to comment card hover states 2026-01-24 06:18:55 +00:00
David Hill
d3490cfd29 feat(ui): add close-small icon and use it for comment card dismiss button 2026-01-24 06:18:55 +00:00
David Hill
5384040051 fix(app): truncate comment text and set card max-width to 200px 2026-01-24 06:18:55 +00:00
David Hill
1bf4caa0c1 fix(app): indent comment text to align with filename in context card 2026-01-24 06:18:55 +00:00
David Hill
35a3c98221 fix(app): style submitted comment icons to match comment popup style 2026-01-24 06:18:55 +00:00
David Hill
56ece04dd5 fix(app): update prompt input styling - 14px border radius, card hover states, and 8px padding 2026-01-24 06:18:55 +00:00
David Hill
328bd3fb02 fix(app): update context cards styling with 8px padding/gap and 6px border radius 2026-01-24 06:18:55 +00:00
David Hill
2daa3652bb fix(ui): add button-primary-base variable and use primary variant for Comment button 2026-01-24 06:18:54 +00:00
David Hill
ae84e9909a fix(app): improve comment popup styling and add new comment icon 2026-01-24 06:18:54 +00:00
Fynn
b978ca11da fix: retry webfetch with simple UA on 403 (#10328) 2026-01-24 00:14:32 -05:00
Github Action
e452b3cae0 chore: update nix node_modules hashes 2026-01-24 05:13:52 +00:00
GitHub Action
e2d8310b76 chore: generate 2026-01-24 05:12:45 +00:00
Daniel Olowoniyi
1d09343f17 fix: allow gpt-5.1-codex model in codex auth pluginFixes (#10181) 2026-01-24 00:11:54 -05:00
Vladimir Glafirov
6633f0e6fa fix: bump gitlab-ai-provider version (#10255) 2026-01-24 00:11:18 -05:00
Shantur Rathore
4173adf5e2 feat(tasks): Add model info as part of metadata (#10307) 2026-01-24 00:10:40 -05:00
Arthur
cf7e10c4e8 fix: add xhigh reasoning effort for GitHub Copilot GPT-5 models (#10092)
Co-authored-by: Arthur Freitas Ramos <arthur@MacBook-Air-de-Arthur.local>
2026-01-24 00:09:47 -05:00
Frank
af5e405391 zen: remove grok code model 2026-01-23 23:25:34 -05:00
Alex Yaroshuk
8a216a6ad5 fix(app): normalize path separators for session diff filtering on Windows (#10291) 2026-01-23 16:17:47 -06:00
Ariane Emory
225b72ca36 feat: always center selected item in selection dialogs (resolves #10209) (#10207) 2026-01-23 11:59:39 -06:00
Rahul A Mistry
8105f186dc fix(app): center checkbox indicator in provider selection (#10267) 2026-01-23 10:23:24 -06:00
GitHub Action
4f1bdf1c59 chore: generate 2026-01-23 16:22:07 +00:00
Frank
472695caca zen: fix balance not shown 2026-01-23 11:21:08 -05:00
GitHub Action
469fd43c71 chore: generate 2026-01-23 15:59:00 +00:00
Frank
24d942349f zen: use balance after rate limited 2026-01-23 10:58:00 -05:00
Edin
65c236c071 feat(app): auto-open oauth links for codex and copilot (#10258) 2026-01-23 09:35:44 -06:00
GitHub Action
d6c5ddd6dc ignore: update download stats 2026-01-23 2026-01-23 12:05:37 +00:00
Adam
e5fe50f7da fix(app): close delete workspace dialog immediately 2026-01-23 05:41:51 -06:00
Adam
b6beda1569 fix: type error 2026-01-23 05:32:37 -06:00
GitHub Action
f34b509fe7 chore: generate 2026-01-23 11:19:35 +00:00
Adam
2a2d800ac4 fix: type error 2026-01-23 05:18:57 -06:00
Adam
4afb46f571 perf(app): don't remount directory layout 2026-01-23 05:18:57 -06:00
Adam
c4d223eb99 perf(app): faster workspace creation 2026-01-23 05:18:42 -06:00
GitHub Action
3fbda54045 chore: generate 2026-01-23 11:10:22 +00:00
Shantur Rathore
41ede06b20 docs(ecosystem): Add CodeNomad entry to ecosystem documentation (#10222) 2026-01-23 05:09:38 -06:00
Adam
82ec84982e Reapply "wip(app): line selection"
This reverts commit df7b6792cd.
2026-01-23 05:01:10 -06:00
Adam
df7b6792cd Revert "wip(app): line selection"
This reverts commit 1780bab1ce.
2026-01-23 04:58:41 -06:00
Devin Griffin
c72d9a473c fix(app): View all sessions flakiness (#10149) 2026-01-23 04:57:10 -06:00
GitHub Action
d3688b150a chore: generate 2026-01-23 10:55:37 +00:00
Rahul A Mistry
e376e1de16 fix(app): enable dialog dismiss on model selector (dialog.tsx) (#10203) 2026-01-23 04:55:00 -06:00
opencode
c130dd425a release: v1.1.34 2026-01-23 07:27:35 +00:00
Eric Guo
b298982268 fix(desktop): Fixed a reactive feedback loop in the global project cache sync (#10139) 2026-01-23 15:23:06 +08:00
Frank
47a2b9e8df zen: glm 4.7 2026-01-23 01:21:41 -05:00
GitHub Action
213b823c69 chore: generate 2026-01-23 05:28:47 +00:00
Frank
c0dc8ea39e wip: zen black 2026-01-23 00:27:54 -05:00
GitHub Action
077ebdbfda chore: generate 2026-01-23 04:12:51 +00:00
Adam
1780bab1ce wip(app): line selection 2026-01-22 22:12:12 -06:00
Adam
d35fabf5db chore: cleanup 2026-01-22 22:12:12 -06:00
Adam
82f718b3cf wip(app): line selection 2026-01-22 22:12:12 -06:00
Adam
0eb523631d wip(app): line selection 2026-01-22 22:12:12 -06:00
Adam
99e15caaf6 wip(app): line selection 2026-01-22 22:12:12 -06:00
Adam
1e1872aada wip(app): line selection 2026-01-22 22:12:12 -06:00
Adam
cb481d9ac8 wip(app): line selection 2026-01-22 22:12:12 -06:00
Adam
0ce0cacb28 wip(app): line selection 2026-01-22 22:12:12 -06:00
Adam
640d1f1ecc wip(app): line selection 2026-01-22 22:12:12 -06:00
opencode
2e53697da0 release: v1.1.33 2026-01-23 02:38:33 +00:00
Adam
71cd59932e fix(app): session shouldn't be keyed 2026-01-22 20:28:10 -06:00
Adam
14db336e3a fix(app): flash of fallback icon for projects 2026-01-22 20:17:50 -06:00
Adam
2b9b98e9c2 fix(app): project icon color flash on load 2026-01-22 20:17:50 -06:00
Adam
07015aae07 fix(app): folder suggestions missing last part 2026-01-22 20:17:50 -06:00
Adam
972cb01d5c fix(app): allow adding projects from any depth 2026-01-22 20:09:18 -06:00
Adam
a8018dcc43 fix(app): allow adding projects from root 2026-01-22 20:09:18 -06:00
zerone0x
31094cd5a4 fix(provider): add thinking presets for Google Vertex Anthropic (#9953)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 19:44:18 -06:00
Adam
bcf7a65e36 fix(app): non-git projects should be renameable 2026-01-22 18:07:57 -06:00
Github Action
7c80ac072b chore: update nix node_modules hashes 2026-01-22 22:29:41 +00:00
Vladimir Glafirov
515391e9c7 feat(gitlab): Added support for OpenAI based GitLab Duo models (#10108) 2026-01-22 16:26:25 -06:00
Idris Gadi
510f595e25 fix(tui): add weight to fuzzy search to maintain title priority (#10106) 2026-01-22 16:15:11 -06:00
Frank
1b244bf850 wip: zen black 2026-01-22 17:06:47 -05:00
GitHub Action
c128579cfc chore: generate 2026-01-22 22:04:55 +00:00
Frank
5f3ab9395f wip: zen black 2026-01-22 17:02:46 -05:00
Alex Yaroshuk
fdac21688c feat(app): add app version display to settings (#10095) 2026-01-22 14:41:29 -06:00
GitHub Action
dd5a601eda chore: generate 2026-01-22 19:43:50 +00:00
iltenahmet
3eaf6f3baf fix(ui): show file path in apply_patch request permission screen (#10079) 2026-01-22 13:43:11 -06:00
opencode
71ef43f9a0 release: v1.1.32 2026-01-22 19:22:26 +00:00
Greg Pstrucha
8ebb766470 fix(attach): allow remote --dir (#8969) 2026-01-22 13:19:08 -06:00
Adam
46de1ed3b6 fix(app): windows path handling issues 2026-01-22 12:41:02 -06:00
Alex Yaroshuk
3c7d5174b3 fix(ui): prevent copy buttons from stealing focus from prompt input (#10084) 2026-01-22 12:37:13 -06:00
Ygor Simões
32f72f49a8 feat(i18n): add br locale support (#10086) 2026-01-22 12:36:33 -06:00
Shubh Porwal
923e3da973 feat(ui): add aura theme (#10056) 2026-01-22 12:28:41 -06:00
GitHub Action
c96c25a72c chore: generate 2026-01-22 18:24:16 +00:00
Aryan "LAG" Gupta
cda7d3dd78 fix: make 'Learn More' link functional in theme settings (#10078) 2026-01-22 12:23:29 -06:00
Adam
9802ceb94f chore: cleanup 2026-01-22 12:22:10 -06:00
Adam
62115832f5 feat(app): render audio players in session review 2026-01-22 12:22:10 -06:00
Adam
496bbd70f4 feat(app): render images in session review 2026-01-22 12:22:10 -06:00
Adam
93044cc7d1 test(app): fix windows paths 2026-01-22 12:19:57 -06:00
GitHub Action
5a4eec5b08 chore: generate 2026-01-22 18:17:10 +00:00
Frank
e17b875641 zen: cancel waitlist 2026-01-22 13:02:28 -05:00
Frank
a890d51bbc wip: zen black 2026-01-22 13:02:28 -05:00
Adam
bb582416f2 chore: cleanup 2026-01-22 11:21:14 -06:00
Adam
b8526eca67 chore: cleanup 2026-01-22 11:19:53 -06:00
Adam
9c45746bd2 fix(app): new session button 2026-01-22 11:17:55 -06:00
Adam
c4971e48c4 chore(app): translations 2026-01-22 11:06:51 -06:00
Adam
de6582b38b feat(app): delete sessions 2026-01-22 11:06:51 -06:00
Adam
fc53abe589 feat(app): close projects from hover card 2026-01-22 11:03:49 -06:00
Adam
7b23bf7c1b fix(app): don't auto nav to workspace after reset 2026-01-22 10:57:43 -06:00
Adam
c0d3dd51b1 chore: upload playwright assets on test failure 2026-01-22 10:45:06 -06:00
Adam
a96f3d153b Revert "fix: handle special characters in paths and git snapshot reading logic(#9804) (#9807)"
This reverts commit cf6ad4c407.
2026-01-22 10:37:13 -06:00
Adam
31f3a508dc Revert "fix(core): snapshot regression"
This reverts commit bb710e9ea1.
2026-01-22 10:35:31 -06:00
Aiden Cline
3b7c347b2e tweak: bash tool, ensure cat will trigger external_directory perm 2026-01-22 10:26:06 -06:00
Aiden Cline
2e09d7d835 ignore: git ignore the lockfiles for .opencode 2026-01-22 10:06:39 -06:00
karta0807913
29cebd73e5 feat(mcp log): print mcp stderr to opencode log file (#9982)
Co-authored-by: chuxuan.liang <chuxuan.liang@bytedance.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-01-22 10:02:26 -06:00
Cas
e4286ae7a3 fix(codex): write refresh tokens to openai auth (#10010) (#10011) 2026-01-22 09:53:09 -06:00
bewareoftheleopard
c3f393bcc1 fix(desktop): Expand font stacks to include macOS Nerd Font default names (#10045) 2026-01-22 08:56:19 -06:00
Ryan Miville
9aa54fd71b fix(app): support ctrl-n/p in lists (#10036) 2026-01-22 08:33:35 -06:00
Yash Rathore
e85b953087 fix(app): clear session hover state on navigation (#10031) 2026-01-22 08:32:13 -06:00
Brendan Allan
b776ba6b76 fix(desktop): correct NO_PROXY syntax 2026-01-22 22:18:08 +08:00
Brendan Allan
224b2c37d7 fix(desktop): attempt to improve connection reliability 2026-01-22 22:06:28 +08:00
GitHub Action
16a8f5a9c3 chore: generate 2026-01-22 13:43:36 +00:00
Adam
16fad51b5e feat(app): add workspace startup script to projects 2026-01-22 07:42:56 -06:00
Adam
287511c9b1 test(app): terminal smoke test 2026-01-22 07:34:44 -06:00
Adam
0a678eeacc test(app): file viewer smoke test 2026-01-22 07:34:44 -06:00
Adam
c031139b89 test(app): model picker smoke test 2026-01-22 07:34:44 -06:00
Adam
710dc4fa94 test(app): @ attachment smoke test 2026-01-22 07:34:44 -06:00
Adam
ec53a7962e test(app): slash command smoke tests 2026-01-22 07:34:44 -06:00
Adam
6f7d710129 test(app): settings smoke tests 2026-01-22 07:34:44 -06:00
Adam
513a8a3d26 test(app): smoke tests spec 2026-01-22 07:34:44 -06:00
Adam
c41c9a366f fix: type error 2026-01-22 07:24:13 -06:00
Adam
4385f03053 fix: satisfies 2026-01-22 07:19:41 -06:00
Adam
8e3b459d77 fix(app): hover-card scrolling 2026-01-22 07:16:02 -06:00
Adam
3807523f49 fix(app): auto-scroll 2026-01-22 07:16:02 -06:00
Adam
09997bb6c8 fix(app): auto-scroll 2026-01-22 07:16:01 -06:00
Alex Yaroshuk
aa17729008 feat(app): add scrollbar styling to session page (#10020) 2026-01-22 06:53:55 -06:00
GitHub Action
b59f3e6811 chore: generate 2026-01-22 12:47:58 +00:00
Sondre
8427f40e8d feat: Add support for Norwegian translations (#10018) 2026-01-22 06:47:19 -06:00
GitHub Action
e9c6a4a2d4 chore: generate 2026-01-22 12:30:20 +00:00
Adam
fb007d6bab feat(app): copy buttons for assistant messages and code blocks 2026-01-22 06:29:38 -06:00
GitHub Action
4ca088ed12 ignore: update download stats 2026-01-22 2026-01-22 12:05:29 +00:00
Adam
ae2693425e fix(app): snap to bottom on prompt 2026-01-22 05:45:53 -06:00
Adam
d9b9485019 fix(app): a11y translations 2026-01-22 05:36:38 -06:00
Rahul A Mistry
366da595af fix(desktop): change project path tooltip position to bottom (#9497) 2026-01-22 05:23:37 -06:00
Ariane Emory
fb3d8e83c5 chore: add plans directory to .opencode gitignore (resolves #10005) (#10006) 2026-01-22 05:19:13 -06:00
GitHub Action
d14735ef4b chore: generate 2026-01-22 11:11:32 +00:00
Nolan Darilek
3435327bc0 fix(app): session screen accessibility improvements (#9907) 2026-01-22 05:10:53 -06:00
Adam
8a043edfd5 chore: update website stats 2026-01-22 04:53:12 -06:00
GitHub Action
de07cf26e8 chore: generate 2026-01-22 10:49:25 +00:00
Shoubhit Dash
c737776958 refactor(desktop): move markdown rendering to rust (#10000) 2026-01-22 04:48:39 -06:00
David Hill
7b0ad87781 fix: add 8px left margin to sidebar toggle on desktop 2026-01-22 09:50:13 +00:00
David Hill
3b92d5c1c6 fix: match terminal toggle button size with sidebar and review toggles 2026-01-22 09:48:10 +00:00
David Hill
cf1fc02d27 update jump to latest button with circular design and animation
- add arrow-down-to-line icon
- circular 32px button centered above prompt input
- fade/scale/translate animation on show/hide
- fix duplicate language.ru keys in i18n files
2026-01-22 09:41:28 +00:00
NourEldin Osama
ba2e35e29c feat(i18n): add Arabic language support (#9947) 2026-01-22 02:14:01 -06:00
DNGriffin
9afc067152 feat(app): always show Toggle-Review button (#9944) 2026-01-22 02:09:55 -06:00
Idris Gadi
9fc182baf2 docs: add API server section in CONTRIBUTING.md (#9888) 2026-01-22 00:36:57 -06:00
Aiden Cline
c2844697f3 fix: ensure images are properly returned as tool results 2026-01-21 23:54:44 -06:00
Alex Sadleir
fc0210c2fd fix(acp): rename setSessionModel to unstable_setSessionModel (#9940) 2026-01-21 22:11:09 -06:00
Caleb Norton
f1df6f2d18 chore: update flake.lock (#9938) 2026-01-21 22:10:55 -06:00
luo jiyin
c3415b79fe fix: correct spelling 'supercedes' to 'supersedes' (#9935)
Signed-off-by: luojiyin <luojiyin@hotmail.com>
2026-01-21 22:10:40 -06:00
Ronan Kearns
af1e2887bd fix(app): open terminal pane when creating new terminal (#9926) 2026-01-21 21:09:08 -06:00
dpuyosa
65e267ed3a feat: Add promptCacheKey for Venice provider (#9915) 2026-01-21 20:28:04 -06:00
Daniel Rodriguez
6d574549bc fix: include _noop tool in activeTools for LiteLLM proxy compatibility (#9912) 2026-01-21 18:46:41 -06:00
GitHub Action
f7c5b62ba3 chore: generate 2026-01-22 00:22:40 +00:00
Ryan Vogel
8c230fee62 fix: scope PR recap to only PRs from today (#9905) 2026-01-22 00:22:10 +00:00
opencode
59ceca3e51 release: v1.1.31 2026-01-22 00:22:10 +00:00
Adam
877b0412c9 fix(app): use message diffs, not session diffs 2026-01-21 18:18:57 -06:00
Ryan Vogel
a0d71bf8ef feat: add daily Discord recaps for issues and PRs (#9904) 2026-01-21 18:35:22 -05:00
Aiden Cline
19fe3e265a mark subagent sessions as agent initiated to ensure they dont count against quota (got the ok from copilot team) 2026-01-21 16:33:43 -06:00
Frank
20b6cc279f zen: show subscription usage in graph 2026-01-21 17:21:34 -05:00
Frank
80c808d186 zen: show subscription usage in usage history 2026-01-21 17:21:34 -05:00
Frank
a132b2a138 Zen: disable autoreload by default 2026-01-21 17:21:34 -05:00
Suad Wolgram
936f3ebe95 feat(ui): add gruvbox theme (Web/App) (#9855) 2026-01-21 15:34:27 -06:00
Alex Yaroshuk
23daac2170 feat(i18n): add Traditional Chinese language support & rename 'Chinese' to 'Chinese (Simplified)' (#9887) 2026-01-21 15:33:26 -06:00
Alex Yaroshuk
383c2787f9 feat(i18n): add Russian language support (#9882) 2026-01-21 15:30:12 -06:00
Aiden Cline
c89f6e7ac6 add chat.headers hook, adjust codex and copilot plugins to use it 2026-01-21 15:10:08 -06:00
GitHub Action
17a5f75b54 chore: generate 2026-01-21 21:05:26 +00:00
Filip
5ca28b6454 feat(app): polish translations (#9884) 2026-01-21 15:04:25 -06:00
280 changed files with 19425 additions and 2893 deletions

166
.github/workflows/daily-issues-recap.yml vendored Normal file
View File

@@ -0,0 +1,166 @@
name: Daily Issues Recap
on:
schedule:
# Run at 6 PM EST (23:00 UTC, or 22:00 UTC during daylight saving)
- cron: "0 23 * * *"
workflow_dispatch: # Allow manual trigger for testing
jobs:
daily-recap:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Generate daily issues recap
id: recap
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: |
{
"bash": {
"*": "deny",
"gh issue*": "allow",
"gh search*": "allow"
},
"webfetch": "deny",
"edit": "deny",
"write": "deny"
}
run: |
# Get today's date range
TODAY=$(date -u +%Y-%m-%d)
opencode run -m opencode/claude-sonnet-4-5 "Generate a daily issues recap for the OpenCode repository.
TODAY'S DATE: ${TODAY}
STEP 1: Gather today's issues
Search for all issues created today (${TODAY}) using:
gh issue list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
STEP 2: Analyze and categorize
For each issue created today, categorize it:
**Severity Assessment:**
- CRITICAL: Crashes, data loss, security issues, blocks major functionality
- HIGH: Significant bugs affecting many users, important features broken
- MEDIUM: Bugs with workarounds, minor features broken
- LOW: Minor issues, cosmetic, nice-to-haves
**Activity Assessment:**
- Note issues with high comment counts or engagement
- Note issues from repeat reporters (check if author has filed before)
STEP 3: Cross-reference with existing issues
For issues that seem like feature requests or recurring bugs:
- Search for similar older issues to identify patterns
- Note if this is a frequently requested feature
- Identify any issues that are duplicates of long-standing requests
STEP 4: Generate the recap
Create a structured recap with these sections:
===DISCORD_START===
**Daily Issues Recap - ${TODAY}**
**Summary Stats**
- Total issues opened today: [count]
- By category: [bugs/features/questions]
**Critical/High Priority Issues**
[List any CRITICAL or HIGH severity issues with brief descriptions and issue numbers]
**Most Active/Discussed**
[Issues with significant engagement or from active community members]
**Trending Topics**
[Patterns noticed - e.g., 'Multiple reports about X', 'Continued interest in Y feature']
**Duplicates & Related**
[Issues that relate to existing open issues]
===DISCORD_END===
STEP 5: Format for Discord
Format the recap as a Discord-compatible message:
- Use Discord markdown (**, __, etc.)
- BE EXTREMELY CONCISE - this is an EOD summary, not a detailed report
- Use hyperlinked issue numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/issues/1234>)
- Group related issues on single lines where possible
- Add emoji sparingly for critical items only
- HARD LIMIT: Keep under 1800 characters total
- Skip sections that have nothing notable (e.g., if no critical issues, omit that section)
- Prioritize signal over completeness - only surface what matters
OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/recap_raw.txt
# Extract only the Discord message between markers
sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/recap_raw.txt | grep -v '===DISCORD' > /tmp/recap.txt
echo "recap_file=/tmp/recap.txt" >> $GITHUB_OUTPUT
- name: Post to Discord
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
run: |
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
cat /tmp/recap.txt
exit 0
fi
# Read the recap
RECAP_RAW=$(cat /tmp/recap.txt)
RECAP_LENGTH=${#RECAP_RAW}
echo "Recap length: ${RECAP_LENGTH} chars"
# Function to post a message to Discord
post_to_discord() {
local msg="$1"
local content=$(echo "$msg" | jq -Rs '.')
curl -s -H "Content-Type: application/json" \
-X POST \
-d "{\"content\": ${content}}" \
"$DISCORD_WEBHOOK_URL"
sleep 1
}
# If under limit, send as single message
if [ "$RECAP_LENGTH" -le 1950 ]; then
post_to_discord "$RECAP_RAW"
else
echo "Splitting into multiple messages..."
remaining="$RECAP_RAW"
while [ ${#remaining} -gt 0 ]; do
if [ ${#remaining} -le 1950 ]; then
post_to_discord "$remaining"
break
else
chunk="${remaining:0:1900}"
last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
chunk="${remaining:0:$last_newline}"
remaining="${remaining:$((last_newline+1))}"
else
chunk="${remaining:0:1900}"
remaining="${remaining:1900}"
fi
post_to_discord "$chunk"
fi
done
fi
echo "Posted daily recap to Discord"

169
.github/workflows/daily-pr-recap.yml vendored Normal file
View File

@@ -0,0 +1,169 @@
name: Daily PR Recap
on:
schedule:
# Run at 5pm EST (22:00 UTC, or 21:00 UTC during daylight saving)
- cron: "0 22 * * *"
workflow_dispatch: # Allow manual trigger for testing
jobs:
pr-recap:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Generate daily PR recap
id: recap
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: |
{
"bash": {
"*": "deny",
"gh pr*": "allow",
"gh search*": "allow"
},
"webfetch": "deny",
"edit": "deny",
"write": "deny"
}
run: |
TODAY=$(date -u +%Y-%m-%d)
opencode run -m opencode/claude-sonnet-4-5 "Generate a daily PR activity recap for the OpenCode repository.
TODAY'S DATE: ${TODAY}
STEP 1: Gather PR data
Run these commands to gather PR information. ONLY include PRs created or updated TODAY (${TODAY}):
# PRs created today
gh pr list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
# PRs with activity today (updated today)
gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
STEP 2: For high-activity PRs, check comment counts
For promising PRs, run:
gh pr view [NUMBER] --repo ${{ github.repository }} --json comments --jq '[.comments[] | select(.author.login != \"copilot-pull-request-reviewer\" and .author.login != \"github-actions\")] | length'
IMPORTANT: When counting comments/activity, EXCLUDE these bot accounts:
- copilot-pull-request-reviewer
- github-actions
STEP 3: Identify what matters (ONLY from today's PRs)
**Bug Fixes From Today:**
- PRs with 'fix' or 'bug' in title created/updated today
- Small bug fixes (< 100 lines changed) that are easy to review
- Bug fixes from community contributors
**High Activity Today:**
- PRs with significant human comments today (excluding bots listed above)
- PRs with back-and-forth discussion today
**Quick Wins:**
- Small PRs (< 50 lines) that are approved or nearly approved
- PRs that just need a final review
STEP 4: Generate the recap
Create a structured recap:
===DISCORD_START===
**Daily PR Recap - ${TODAY}**
**New PRs Today**
[PRs opened today - group by type: bug fixes, features, etc.]
**Active PRs Today**
[PRs with activity/updates today - significant discussion]
**Quick Wins**
[Small PRs ready to merge]
===DISCORD_END===
STEP 5: Format for Discord
- Use Discord markdown (**, __, etc.)
- BE EXTREMELY CONCISE - surface what we might miss
- Use hyperlinked PR numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/pull/1234>)
- Include PR author: [#1234](<url>) (@author)
- For bug fixes, add brief description of what it fixes
- Show line count for quick wins: \"(+15/-3 lines)\"
- HARD LIMIT: Keep under 1800 characters total
- Skip empty sections
- Focus on PRs that need human eyes
OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/pr_recap_raw.txt
# Extract only the Discord message between markers
sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/pr_recap_raw.txt | grep -v '===DISCORD' > /tmp/pr_recap.txt
echo "recap_file=/tmp/pr_recap.txt" >> $GITHUB_OUTPUT
- name: Post to Discord
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
run: |
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
cat /tmp/pr_recap.txt
exit 0
fi
# Read the recap
RECAP_RAW=$(cat /tmp/pr_recap.txt)
RECAP_LENGTH=${#RECAP_RAW}
echo "Recap length: ${RECAP_LENGTH} chars"
# Function to post a message to Discord
post_to_discord() {
local msg="$1"
local content=$(echo "$msg" | jq -Rs '.')
curl -s -H "Content-Type: application/json" \
-X POST \
-d "{\"content\": ${content}}" \
"$DISCORD_WEBHOOK_URL"
sleep 1
}
# If under limit, send as single message
if [ "$RECAP_LENGTH" -le 1950 ]; then
post_to_discord "$RECAP_RAW"
else
echo "Splitting into multiple messages..."
remaining="$RECAP_RAW"
while [ ${#remaining} -gt 0 ]; do
if [ ${#remaining} -le 1950 ]; then
post_to_discord "$remaining"
break
else
chunk="${remaining:0:1900}"
last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
chunk="${remaining:0:$last_newline}"
remaining="${remaining:$((last_newline+1))}"
else
chunk="${remaining:0:1900}"
remaining="${remaining:1900}"
fi
post_to_discord "$chunk"
fi
done
fi
echo "Posted daily PR recap to Discord"

View File

@@ -53,7 +53,6 @@ jobs:
printf '%s\n' "XDG_CACHE_HOME=${{ runner.temp }}\\opencode-e2e\\cache" >> "$GITHUB_ENV"
printf '%s\n' "XDG_CONFIG_HOME=${{ runner.temp }}\\opencode-e2e\\config" >> "$GITHUB_ENV"
printf '%s\n' "XDG_STATE_HOME=${{ runner.temp }}\\opencode-e2e\\state" >> "$GITHUB_ENV"
printf '%s\n' "MODELS_DEV_API_JSON=${{ github.workspace }}\\packages\\opencode\\test\\tool\\fixtures\\models-api.json" >> "$GITHUB_ENV"
else
printf '%s\n' "OPENCODE_E2E_ROOT=${{ runner.temp }}/opencode-e2e" >> "$GITHUB_ENV"
printf '%s\n' "OPENCODE_TEST_HOME=${{ runner.temp }}/opencode-e2e/home" >> "$GITHUB_ENV"
@@ -61,7 +60,6 @@ jobs:
printf '%s\n' "XDG_CACHE_HOME=${{ runner.temp }}/opencode-e2e/cache" >> "$GITHUB_ENV"
printf '%s\n' "XDG_CONFIG_HOME=${{ runner.temp }}/opencode-e2e/config" >> "$GITHUB_ENV"
printf '%s\n' "XDG_STATE_HOME=${{ runner.temp }}/opencode-e2e/state" >> "$GITHUB_ENV"
printf '%s\n' "MODELS_DEV_API_JSON=${{ github.workspace }}/packages/opencode/test/tool/fixtures/models-api.json" >> "$GITHUB_ENV"
fi
- name: Seed opencode data
@@ -69,8 +67,6 @@ jobs:
working-directory: packages/opencode
run: bun script/seed-e2e.ts
env:
MODELS_DEV_API_JSON: ${{ env.MODELS_DEV_API_JSON }}
OPENCODE_DISABLE_MODELS_FETCH: "true"
OPENCODE_DISABLE_SHARE: "true"
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
@@ -90,8 +86,6 @@ jobs:
working-directory: packages/opencode
run: bun dev -- --print-logs --log-level WARN serve --port 4096 --hostname 127.0.0.1 &
env:
MODELS_DEV_API_JSON: ${{ env.MODELS_DEV_API_JSON }}
OPENCODE_DISABLE_MODELS_FETCH: "true"
OPENCODE_DISABLE_SHARE: "true"
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
@@ -117,8 +111,6 @@ jobs:
run: ${{ matrix.settings.command }}
env:
CI: true
MODELS_DEV_API_JSON: ${{ env.MODELS_DEV_API_JSON }}
OPENCODE_DISABLE_MODELS_FETCH: "true"
OPENCODE_DISABLE_SHARE: "true"
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
@@ -134,3 +126,14 @@ jobs:
VITE_OPENCODE_SERVER_PORT: "4096"
OPENCODE_CLIENT: "app"
timeout-minutes: 30
- name: Upload Playwright artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-${{ matrix.settings.name }}-${{ github.run_attempt }}
if-no-files-found: ignore
retention-days: 7
path: |
packages/app/e2e/test-results
packages/app/e2e/playwright-report

3
.opencode/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
plans/
bun.lock
package.json

View File

@@ -1,6 +1,6 @@
---
description: git commit and push
model: opencode/glm-4.6
model: opencode/glm-4.7
subtask: true
---
@@ -26,3 +26,15 @@ about what user facing changes were made
if there are changes do a git pull --rebase
if there are conflicts DO NOT FIX THEM. notify me and I will fix them
## GIT DIFF
!`git diff`
## GIT DIFF --cached
!`git diff --cached`
## GIT STATUS --short
!`git status --short`

View File

@@ -4,7 +4,6 @@
// "enterprise": {
// "url": "https://enterprise.dev.opencode.ai",
// },
"instructions": ["STYLE_GUIDE.md"],
"provider": {
"opencode": {
"options": {},

View File

@@ -1,4 +1,81 @@
- To test opencode in `packages/opencode`, run `bun dev`.
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
- The default branch in this repo is `dev`.
## Style Guide
- Keep things in one function unless composable or reusable
- Avoid unnecessary destructuring. Instead of `const { a, b } = obj`, use `obj.a` and `obj.b` to preserve context
- Avoid `try`/`catch` where possible
- Avoid using the `any` type
- Prefer single word variable names where possible
- Use Bun APIs when possible, like `Bun.file()`
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
### Avoid let statements
We don't like `let` statements, especially combined with if/else statements.
Prefer `const`.
Good:
```ts
const foo = condition ? 1 : 2
```
Bad:
```ts
let foo
if (condition) foo = 1
else foo = 2
```
### Avoid else statements
Prefer early returns or using an `iife` to avoid else statements.
Good:
```ts
function foo() {
if (condition) return 1
return 2
}
```
Bad:
```ts
function foo() {
if (condition) return 1
else return 2
}
```
### Prefer single word naming
Try your best to find a single word name for your variables, functions, etc.
Only use multiple words if you cannot.
Good:
```ts
const foo = 1
const bar = 2
const baz = 3
```
Bad:
```ts
const fooBar = 1
const barBaz = 2
const bazFoo = 3
```
## Testing
You MUST avoid using `mocks` as much as possible.
Tests MUST test actual implementation, do not duplicate logic into a test.

View File

@@ -71,15 +71,50 @@ Replace `<platform>` with your platform (e.g., `darwin-arm64`, `linux-x64`).
- `packages/desktop`: The native desktop app, built with Tauri (wraps `packages/app`)
- `packages/plugin`: Source for `@opencode-ai/plugin`
### Understanding bun dev vs opencode
During development, `bun dev` is the local equivalent of the built `opencode` command. Both run the same CLI interface:
```bash
# Development (from project root)
bun dev --help # Show all available commands
bun dev serve # Start headless API server
bun dev web # Start server + open web interface
bun dev <directory> # Start TUI in specific directory
# Production
opencode --help # Show all available commands
opencode serve # Start headless API server
opencode web # Start server + open web interface
opencode <directory> # Start TUI in specific directory
```
### Running the API Server
To start the OpenCode headless API server:
```bash
bun dev serve
```
This starts the headless server on port 4096 by default. You can specify a different port:
```bash
bun dev serve --port 8080
```
### Running the Web App
To test UI changes during development, run the web app:
To test UI changes during development:
1. **First, start the OpenCode server** (see [Running the API Server](#running-the-api-server) section above)
2. **Then run the web app:**
```bash
bun run --cwd packages/app dev
```
This starts a local dev server at http://localhost:5173 (or similar port shown in output). Most UI changes can be tested here.
This starts a local dev server at http://localhost:5173 (or similar port shown in output). Most UI changes can be tested here, but the server must be running for full functionality.
### Running the Desktop App
@@ -113,7 +148,7 @@ This runs `bun run --cwd packages/desktop build` automatically via Tauris `be
> [!NOTE]
> If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files.
Please try to follow the [style guide](./STYLE_GUIDE.md)
Please try to follow the [style guide](./AGENTS.md)
### Setting up a Debugger
@@ -127,9 +162,9 @@ Caveats:
- If you want to run the OpenCode TUI and have breakpoints triggered in the server code, you might need to run `bun dev spawn` instead of
the usual `bun dev`. This is because `bun dev` runs the server in a worker thread and breakpoints might not work there.
- If `spawn` does not work for you, you can debug the server separately:
- Debug server: `bun run --inspect=ws://localhost:6499/ ./src/index.ts serve --port 4096`,
- Debug server: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts serve --port 4096`,
then attach TUI with `opencode attach http://localhost:4096`
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --conditions=browser ./src/index.ts`
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode --conditions=browser ./src/index.ts`
Other tips and tricks:

132
README.ar.md Normal file
View File

@@ -0,0 +1,132 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="شعار OpenCode">
</picture>
</a>
</p>
<p align="center">وكيل برمجة بالذكاء الاصطناعي مفتوح المصدر.</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### التثبيت
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# مديري الحزم
npm i -g opencode-ai@latest # او bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS و Linux (موصى به، دائما محدث)
brew install opencode # macOS و Linux (صيغة brew الرسمية، تحديث اقل)
paru -S opencode-bin # Arch Linux
mise use -g opencode # اي نظام
nix run nixpkgs#opencode # او github:anomalyco/opencode لاحدث فرع dev
```
> [!TIP]
> احذف الاصدارات الاقدم من 0.1.x قبل التثبيت.
### تطبيق سطح المكتب (BETA)
يتوفر OpenCode ايضا كتطبيق سطح مكتب. قم بالتنزيل مباشرة من [صفحة الاصدارات](https://github.com/anomalyco/opencode/releases) او من [opencode.ai/download](https://opencode.ai/download).
| المنصة | التنزيل |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb` او `.rpm` او AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### مجلد التثبيت
يحترم سكربت التثبيت ترتيب الاولوية التالي لمسار التثبيت:
1. `$OPENCODE_INSTALL_DIR` - مجلد تثبيت مخصص
2. `$XDG_BIN_DIR` - مسار متوافق مع مواصفات XDG Base Directory
3. `$HOME/bin` - مجلد الثنائيات القياسي للمستخدم (ان وجد او امكن انشاؤه)
4. `$HOME/.opencode/bin` - المسار الافتراضي الاحتياطي
```bash
# امثلة
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
يتضمن OpenCode وكيليْن (Agents) مدمجين يمكنك التبديل بينهما باستخدام زر `Tab`.
- **build** - الافتراضي، وكيل بصلاحيات كاملة لاعمال التطوير
- **plan** - وكيل للقراءة فقط للتحليل واستكشاف الكود
- يرفض تعديل الملفات افتراضيا
- يطلب الاذن قبل تشغيل اوامر bash
- مثالي لاستكشاف قواعد كود غير مألوفة او لتخطيط التغييرات
بالاضافة الى ذلك يوجد وكيل فرعي **general** للبحث المعقد والمهام متعددة الخطوات.
يستخدم داخليا ويمكن استدعاؤه بكتابة `@general` في الرسائل.
تعرف على المزيد حول [agents](https://opencode.ai/docs/agents).
### التوثيق
لمزيد من المعلومات حول كيفية ضبط OpenCode، [**راجع التوثيق**](https://opencode.ai/docs).
### المساهمة
اذا كنت مهتما بالمساهمة في OpenCode، يرجى قراءة [contributing docs](./CONTRIBUTING.md) قبل ارسال pull request.
### البناء فوق OpenCode
اذا كنت تعمل على مشروع مرتبط بـ OpenCode ويستخدم "opencode" كجزء من اسمه (مثل "opencode-dashboard" او "opencode-mobile")، يرجى اضافة ملاحظة في README توضح انه ليس مبنيا بواسطة فريق OpenCode ولا يرتبط بنا بأي شكل.
### FAQ
#### ما الفرق عن Claude Code؟
هو مشابه جدا لـ Claude Code من حيث القدرات. هذه هي الفروقات الاساسية:
- 100% مفتوح المصدر
- غير مقترن بمزود معين. نوصي بالنماذج التي نوفرها عبر [OpenCode Zen](https://opencode.ai/zen)؛ لكن يمكن استخدام OpenCode مع Claude او OpenAI او Google او حتى نماذج محلية. مع تطور النماذج ستتقلص الفجوات وستنخفض الاسعار، لذا من المهم ان يكون مستقلا عن المزود.
- دعم LSP جاهز للاستخدام
- تركيز على TUI. تم بناء OpenCode بواسطة مستخدمي neovim ومنشئي [terminal.shop](https://terminal.shop)؛ وسندفع حدود ما هو ممكن داخل الطرفية.
- معمارية عميل/خادم. على سبيل المثال، يمكن تشغيل OpenCode على جهازك بينما تقوده عن بعد من تطبيق جوال. هذا يعني ان واجهة TUI هي واحدة فقط من العملاء الممكنين.
---
**انضم الى مجتمعنا** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

132
README.br.md Normal file
View File

@@ -0,0 +1,132 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="Logo do OpenCode">
</picture>
</a>
</p>
<p align="center">O agente de programação com IA de código aberto.</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Instalação
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Gerenciadores de pacotes
npm i -g opencode-ai@latest # ou bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS e Linux (recomendado, sempre atualizado)
brew install opencode # macOS e Linux (fórmula oficial do brew, atualiza menos)
paru -S opencode-bin # Arch Linux
mise use -g opencode # qualquer sistema
nix run nixpkgs#opencode # ou github:anomalyco/opencode para a branch dev mais recente
```
> [!TIP]
> Remova versões anteriores a 0.1.x antes de instalar.
### App desktop (BETA)
O OpenCode também está disponível como aplicativo desktop. Baixe diretamente pela [página de releases](https://github.com/anomalyco/opencode/releases) ou em [opencode.ai/download](https://opencode.ai/download).
| Plataforma | Download |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` ou AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### Diretório de instalação
O script de instalação respeita a seguinte ordem de prioridade para o caminho de instalação:
1. `$OPENCODE_INSTALL_DIR` - Diretório de instalação personalizado
2. `$XDG_BIN_DIR` - Caminho compatível com a especificação XDG Base Directory
3. `$HOME/bin` - Diretório binário padrão do usuário (se existir ou puder ser criado)
4. `$HOME/.opencode/bin` - Fallback padrão
```bash
# Exemplos
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
O OpenCode inclui dois agents integrados, que você pode alternar com a tecla `Tab`.
- **build** - Padrão, agent com acesso total para trabalho de desenvolvimento
- **plan** - Agent somente leitura para análise e exploração de código
- Nega edições de arquivos por padrão
- Pede permissão antes de executar comandos bash
- Ideal para explorar codebases desconhecidas ou planejar mudanças
Também há um subagent **general** para buscas complexas e tarefas em várias etapas.
Ele é usado internamente e pode ser invocado com `@general` nas mensagens.
Saiba mais sobre [agents](https://opencode.ai/docs/agents).
### Documentação
Para mais informações sobre como configurar o OpenCode, [**veja nossa documentação**](https://opencode.ai/docs).
### Contribuir
Se você tem interesse em contribuir com o OpenCode, leia os [contributing docs](./CONTRIBUTING.md) antes de enviar um pull request.
### Construindo com OpenCode
Se você estiver trabalhando em um projeto relacionado ao OpenCode e estiver usando "opencode" como parte do nome (por exemplo, "opencode-dashboard" ou "opencode-mobile"), adicione uma nota no README para deixar claro que não foi construído pela equipe do OpenCode e não é afiliado a nós de nenhuma forma.
### FAQ
#### Como isso é diferente do Claude Code?
É muito parecido com o Claude Code em termos de capacidade. Aqui estão as principais diferenças:
- 100% open source
- Não está acoplado a nenhum provedor. Embora recomendemos os modelos que oferecemos pelo [OpenCode Zen](https://opencode.ai/zen); o OpenCode pode ser usado com Claude, OpenAI, Google ou até modelos locais. À medida que os modelos evoluem, as diferenças diminuem e os preços caem, então ser provider-agnostic é importante.
- Suporte a LSP pronto para uso
- Foco em TUI. O OpenCode é construído por usuários de neovim e pelos criadores do [terminal.shop](https://terminal.shop); vamos levar ao limite o que é possível no terminal.
- Arquitetura cliente/servidor. Isso, por exemplo, permite executar o OpenCode no seu computador enquanto você o controla remotamente por um aplicativo mobile. Isso significa que o frontend TUI é apenas um dos possíveis clientes.
---
**Junte-se à nossa comunidade** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

132
README.da.md Normal file
View File

@@ -0,0 +1,132 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
</picture>
</a>
</p>
<p align="center">Den open source AI-kodeagent.</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Installation
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Pakkehåndteringer
npm i -g opencode-ai@latest # eller bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS og Linux (anbefalet, altid up to date)
brew install opencode # macOS og Linux (officiel brew formula, opdateres sjældnere)
paru -S opencode-bin # Arch Linux
mise use -g opencode # alle OS
nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste dev-branch
```
> [!TIP]
> Fjern versioner ældre end 0.1.x før installation.
### Desktop-app (BETA)
OpenCode findes også som desktop-app. Download direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download).
| Platform | Download |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, eller AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### Installationsmappe
Installationsscriptet bruger følgende prioriteringsrækkefølge for installationsstien:
1. `$OPENCODE_INSTALL_DIR` - Tilpasset installationsmappe
2. `$XDG_BIN_DIR` - Sti der følger XDG Base Directory Specification
3. `$HOME/bin` - Standard bruger-bin-mappe (hvis den findes eller kan oprettes)
4. `$HOME/.opencode/bin` - Standard fallback
```bash
# Eksempler
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
OpenCode har to indbyggede agents, som du kan skifte mellem med `Tab`-tasten.
- **build** - Standard, agent med fuld adgang til udviklingsarbejde
- **plan** - Skrivebeskyttet agent til analyse og kodeudforskning
- Afviser filredigering som standard
- Spørger om tilladelse før bash-kommandoer
- Ideel til at udforske ukendte kodebaser eller planlægge ændringer
Derudover findes der en **general**-subagent til komplekse søgninger og flertrinsopgaver.
Den bruges internt og kan kaldes via `@general` i beskeder.
Læs mere om [agents](https://opencode.ai/docs/agents).
### Dokumentation
For mere info om konfiguration af OpenCode, [**se vores docs**](https://opencode.ai/docs).
### Bidrag
Hvis du vil bidrage til OpenCode, så læs vores [contributing docs](./CONTRIBUTING.md) før du sender en pull request.
### Bygget på OpenCode
Hvis du arbejder på et projekt der er relateret til OpenCode og bruger "opencode" som en del af navnet; f.eks. "opencode-dashboard" eller "opencode-mobile", så tilføj en note i din README, der tydeliggør at projektet ikke er bygget af OpenCode-teamet og ikke er tilknyttet os på nogen måde.
### FAQ
#### Hvordan adskiller dette sig fra Claude Code?
Det minder meget om Claude Code i forhold til funktionalitet. Her er de vigtigste forskelle:
- 100% open source
- Ikke låst til en udbyder. Selvom vi anbefaler modellerne via [OpenCode Zen](https://opencode.ai/zen); kan OpenCode bruges med Claude, OpenAI, Google eller endda lokale modeller. Efterhånden som modeller udvikler sig vil forskellene mindskes og priserne falde, så det er vigtigt at være provider-agnostic.
- LSP-support out of the box
- Fokus på TUI. OpenCode er bygget af neovim-brugere og skaberne af [terminal.shop](https://terminal.shop); vi vil skubbe grænserne for hvad der er muligt i terminalen.
- Klient/server-arkitektur. Det kan f.eks. lade OpenCode køre på din computer, mens du styrer den eksternt fra en mobilapp. Det betyder at TUI-frontend'en kun er en af de mulige clients.
---
**Bliv en del af vores community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

132
README.de.md Normal file
View File

@@ -0,0 +1,132 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
</picture>
</a>
</p>
<p align="center">Der Open-Source KI-Coding-Agent.</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Installation
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Paketmanager
npm i -g opencode-ai@latest # oder bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS und Linux (empfohlen, immer aktuell)
brew install opencode # macOS und Linux (offizielle Brew-Formula, seltener aktualisiert)
paru -S opencode-bin # Arch Linux
mise use -g opencode # jedes Betriebssystem
nix run nixpkgs#opencode # oder github:anomalyco/opencode für den neuesten dev-Branch
```
> [!TIP]
> Entferne Versionen älter als 0.1.x vor der Installation.
### Desktop-App (BETA)
OpenCode ist auch als Desktop-Anwendung verfügbar. Lade sie direkt von der [Releases-Seite](https://github.com/anomalyco/opencode/releases) oder [opencode.ai/download](https://opencode.ai/download) herunter.
| Plattform | Download |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` oder AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### Installationsverzeichnis
Das Installationsskript beachtet die folgende Prioritätsreihenfolge für den Installationspfad:
1. `$OPENCODE_INSTALL_DIR` - Benutzerdefiniertes Installationsverzeichnis
2. `$XDG_BIN_DIR` - XDG Base Directory Specification-konformer Pfad
3. `$HOME/bin` - Standard-Binärverzeichnis des Users (falls vorhanden oder erstellbar)
4. `$HOME/.opencode/bin` - Standard-Fallback
```bash
# Beispiele
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
OpenCode enthält zwei eingebaute Agents, zwischen denen du mit der `Tab`-Taste wechseln kannst.
- **build** - Standard-Agent mit vollem Zugriff für Entwicklungsarbeit
- **plan** - Nur-Lese-Agent für Analyse und Code-Exploration
- Verweigert Datei-Edits standardmäßig
- Fragt vor dem Ausführen von bash-Befehlen nach
- Ideal zum Erkunden unbekannter Codebases oder zum Planen von Änderungen
Außerdem ist ein **general**-Subagent für komplexe Suchen und mehrstufige Aufgaben enthalten.
Dieser wird intern genutzt und kann in Nachrichten mit `@general` aufgerufen werden.
Mehr dazu unter [Agents](https://opencode.ai/docs/agents).
### Dokumentation
Mehr Infos zur Konfiguration von OpenCode findest du in unseren [**Docs**](https://opencode.ai/docs).
### Beitragen
Wenn du zu OpenCode beitragen möchtest, lies bitte unsere [Contributing Docs](./CONTRIBUTING.md), bevor du einen Pull Request einreichst.
### Auf OpenCode aufbauen
Wenn du an einem Projekt arbeitest, das mit OpenCode zusammenhängt und "opencode" als Teil seines Namens verwendet (z.B. "opencode-dashboard" oder "opencode-mobile"), füge bitte einen Hinweis in deine README ein, dass es nicht vom OpenCode-Team gebaut wird und nicht in irgendeiner Weise mit uns verbunden ist.
### FAQ
#### Worin unterscheidet sich das von Claude Code?
In Bezug auf die Fähigkeiten ist es Claude Code sehr ähnlich. Hier sind die wichtigsten Unterschiede:
- 100% open source
- Nicht an einen Anbieter gekoppelt. Wir empfehlen die Modelle aus [OpenCode Zen](https://opencode.ai/zen); OpenCode kann aber auch mit Claude, OpenAI, Google oder sogar lokalen Modellen genutzt werden. Mit der Weiterentwicklung der Modelle werden die Unterschiede kleiner und die Preise sinken, deshalb ist Provider-Unabhängigkeit wichtig.
- LSP-Unterstützung direkt nach dem Start
- Fokus auf TUI. OpenCode wird von Neovim-Nutzern und den Machern von [terminal.shop](https://terminal.shop) gebaut; wir treiben die Grenzen dessen, was im Terminal möglich ist.
- Client/Server-Architektur. Das ermöglicht z.B., OpenCode auf deinem Computer laufen zu lassen, während du es von einer mobilen App aus fernsteuerst. Das TUI-Frontend ist nur einer der möglichen Clients.
---
**Tritt unserer Community bei** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

132
README.es.md Normal file
View File

@@ -0,0 +1,132 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
</picture>
</a>
</p>
<p align="center">El agente de programación con IA de código abierto.</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Instalación
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Gestores de paquetes
npm i -g opencode-ai@latest # o bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS y Linux (recomendado, siempre al día)
brew install opencode # macOS y Linux (fórmula oficial de brew, se actualiza menos)
paru -S opencode-bin # Arch Linux
mise use -g opencode # cualquier sistema
nix run nixpkgs#opencode # o github:anomalyco/opencode para la rama dev más reciente
```
> [!TIP]
> Elimina versiones anteriores a 0.1.x antes de instalar.
### App de escritorio (BETA)
OpenCode también está disponible como aplicación de escritorio. Descárgala directamente desde la [página de releases](https://github.com/anomalyco/opencode/releases) o desde [opencode.ai/download](https://opencode.ai/download).
| Plataforma | Descarga |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, o AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### Directorio de instalación
El script de instalación respeta el siguiente orden de prioridad para la ruta de instalación:
1. `$OPENCODE_INSTALL_DIR` - Directorio de instalación personalizado
2. `$XDG_BIN_DIR` - Ruta compatible con la especificación XDG Base Directory
3. `$HOME/bin` - Directorio binario estándar del usuario (si existe o se puede crear)
4. `$HOME/.opencode/bin` - Alternativa por defecto
```bash
# Ejemplos
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
OpenCode incluye dos agents integrados que puedes alternar con la tecla `Tab`.
- **build** - Por defecto, agent con acceso completo para trabajo de desarrollo
- **plan** - Agent de solo lectura para análisis y exploración de código
- Niega ediciones de archivos por defecto
- Pide permiso antes de ejecutar comandos bash
- Ideal para explorar codebases desconocidas o planificar cambios
Además, incluye un subagent **general** para búsquedas complejas y tareas de varios pasos.
Se usa internamente y se puede invocar con `@general` en los mensajes.
Más información sobre [agents](https://opencode.ai/docs/agents).
### Documentación
Para más información sobre cómo configurar OpenCode, [**ve a nuestra documentación**](https://opencode.ai/docs).
### Contribuir
Si te interesa contribuir a OpenCode, lee nuestras [docs de contribución](./CONTRIBUTING.md) antes de enviar un pull request.
### Construyendo sobre OpenCode
Si estás trabajando en un proyecto relacionado con OpenCode y usas "opencode" como parte del nombre; por ejemplo, "opencode-dashboard" u "opencode-mobile", agrega una nota en tu README para aclarar que no está construido por el equipo de OpenCode y que no está afiliado con nosotros de ninguna manera.
### FAQ
#### ¿En qué se diferencia de Claude Code?
Es muy similar a Claude Code en cuanto a capacidades. Estas son las diferencias clave:
- 100% open source
- No está acoplado a ningún proveedor. Aunque recomendamos los modelos que ofrecemos a través de [OpenCode Zen](https://opencode.ai/zen); OpenCode se puede usar con Claude, OpenAI, Google o incluso modelos locales. A medida que evolucionan los modelos, las brechas se cerrarán y los precios bajarán, por lo que ser agnóstico al proveedor es importante.
- Soporte LSP listo para usar
- Un enfoque en la TUI. OpenCode está construido por usuarios de neovim y los creadores de [terminal.shop](https://terminal.shop); vamos a empujar los límites de lo que es posible en la terminal.
- Arquitectura cliente/servidor. Esto, por ejemplo, permite ejecutar OpenCode en tu computadora mientras lo controlas de forma remota desde una app móvil. Esto significa que el frontend TUI es solo uno de los posibles clientes.
---
**Únete a nuestra comunidad** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

132
README.fr.md Normal file
View File

@@ -0,0 +1,132 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="Logo OpenCode">
</picture>
</a>
</p>
<p align="center">L'agent de codage IA open source.</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Installation
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Gestionnaires de paquets
npm i -g opencode-ai@latest # ou bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS et Linux (recommandé, toujours à jour)
brew install opencode # macOS et Linux (formule officielle brew, mise à jour moins fréquente)
paru -S opencode-bin # Arch Linux
mise use -g opencode # n'importe quel OS
nix run nixpkgs#opencode # ou github:anomalyco/opencode pour la branche dev la plus récente
```
> [!TIP]
> Supprimez les versions antérieures à 0.1.x avant d'installer.
### Application de bureau (BETA)
OpenCode est aussi disponible en application de bureau. Téléchargez-la directement depuis la [page des releases](https://github.com/anomalyco/opencode/releases) ou [opencode.ai/download](https://opencode.ai/download).
| Plateforme | Téléchargement |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, ou AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### Répertoire d'installation
Le script d'installation respecte l'ordre de priorité suivant pour le chemin d'installation :
1. `$OPENCODE_INSTALL_DIR` - Répertoire d'installation personnalisé
2. `$XDG_BIN_DIR` - Chemin conforme à la spécification XDG Base Directory
3. `$HOME/bin` - Répertoire binaire utilisateur standard (s'il existe ou peut être créé)
4. `$HOME/.opencode/bin` - Repli par défaut
```bash
# Exemples
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
OpenCode inclut deux agents intégrés que vous pouvez basculer avec la touche `Tab`.
- **build** - Par défaut, agent avec accès complet pour le travail de développement
- **plan** - Agent en lecture seule pour l'analyse et l'exploration du code
- Refuse les modifications de fichiers par défaut
- Demande l'autorisation avant d'exécuter des commandes bash
- Idéal pour explorer une base de code inconnue ou planifier des changements
Un sous-agent **general** est aussi inclus pour les recherches complexes et les tâches en plusieurs étapes.
Il est utilisé en interne et peut être invoqué via `@general` dans les messages.
En savoir plus sur les [agents](https://opencode.ai/docs/agents).
### Documentation
Pour plus d'informations sur la configuration d'OpenCode, [**consultez notre documentation**](https://opencode.ai/docs).
### Contribuer
Si vous souhaitez contribuer à OpenCode, lisez nos [docs de contribution](./CONTRIBUTING.md) avant de soumettre une pull request.
### Construire avec OpenCode
Si vous travaillez sur un projet lié à OpenCode et que vous utilisez "opencode" dans le nom du projet (par exemple, "opencode-dashboard" ou "opencode-mobile"), ajoutez une note dans votre README pour préciser qu'il n'est pas construit par l'équipe OpenCode et qu'il n'est pas affilié à nous.
### FAQ
#### En quoi est-ce différent de Claude Code ?
C'est très similaire à Claude Code en termes de capacités. Voici les principales différences :
- 100% open source
- Pas couplé à un fournisseur. Nous recommandons les modèles proposés via [OpenCode Zen](https://opencode.ai/zen) ; OpenCode peut être utilisé avec Claude, OpenAI, Google ou même des modèles locaux. Au fur et à mesure que les modèles évoluent, les écarts se réduiront et les prix baisseront, donc être agnostique au fournisseur est important.
- Support LSP prêt à l'emploi
- Un focus sur la TUI. OpenCode est construit par des utilisateurs de neovim et les créateurs de [terminal.shop](https://terminal.shop) ; nous allons repousser les limites de ce qui est possible dans le terminal.
- Architecture client/serveur. Cela permet par exemple de faire tourner OpenCode sur votre ordinateur tout en le pilotant à distance depuis une application mobile. Cela signifie que la TUI n'est qu'un des clients possibles.
---
**Rejoignez notre communauté** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

132
README.ja.md Normal file
View File

@@ -0,0 +1,132 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
</picture>
</a>
</p>
<p align="center">オープンソースのAIコーディングエージェント。</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### インストール
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# パッケージマネージャー
npm i -g opencode-ai@latest # bun/pnpm/yarn でもOK
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS と Linux推奨。常に最新
brew install opencode # macOS と Linux公式 brew formula。更新頻度は低め
paru -S opencode-bin # Arch Linux
mise use -g opencode # どのOSでも
nix run nixpkgs#opencode # または github:anomalyco/opencode で最新 dev ブランチ
```
> [!TIP]
> インストール前に 0.1.x より古いバージョンを削除してください。
### デスクトップアプリ (BETA)
OpenCode はデスクトップアプリとしても利用できます。[releases page](https://github.com/anomalyco/opencode/releases) から直接ダウンロードするか、[opencode.ai/download](https://opencode.ai/download) を利用してください。
| プラットフォーム | ダウンロード |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb``.rpm`、または AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### インストールディレクトリ
インストールスクリプトは、インストール先パスを次の優先順位で決定します。
1. `$OPENCODE_INSTALL_DIR` - カスタムのインストールディレクトリ
2. `$XDG_BIN_DIR` - XDG Base Directory Specification に準拠したパス
3. `$HOME/bin` - 標準のユーザー用バイナリディレクトリ(存在する場合、または作成できる場合)
4. `$HOME/.opencode/bin` - デフォルトのフォールバック
```bash
# 例
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
OpenCode には組み込みの Agent が2つあり、`Tab` キーで切り替えられます。
- **build** - デフォルト。開発向けのフルアクセス Agent
- **plan** - 分析とコード探索向けの読み取り専用 Agent
- デフォルトでファイル編集を拒否
- bash コマンド実行前に確認
- 未知のコードベース探索や変更計画に最適
また、複雑な検索やマルチステップのタスク向けに **general** サブ Agent も含まれています。
内部的に使用されており、メッセージで `@general` と入力して呼び出せます。
[agents](https://opencode.ai/docs/agents) の詳細はこちら。
### ドキュメント
OpenCode の設定については [**ドキュメント**](https://opencode.ai/docs) を参照してください。
### コントリビュート
OpenCode に貢献したい場合は、Pull Request を送る前に [contributing docs](./CONTRIBUTING.md) を読んでください。
### OpenCode の上に構築する
OpenCode に関連するプロジェクトで、名前に "opencode"(例: "opencode-dashboard" や "opencode-mobile")を含める場合は、そのプロジェクトが OpenCode チームによって作られたものではなく、いかなる形でも関係がないことを README に明記してください。
### FAQ
#### Claude Code との違いは?
機能面では Claude Code と非常に似ています。主な違いは次のとおりです。
- 100% オープンソース
- 特定のプロバイダーに依存しません。[OpenCode Zen](https://opencode.ai/zen) で提供しているモデルを推奨しますが、OpenCode は Claude、OpenAI、Google、またはローカルモデルでも利用できます。モデルが進化すると差は縮まり価格も下がるため、provider-agnostic であることが重要です。
- そのまま使える LSP サポート
- TUI にフォーカス。OpenCode は neovim ユーザーと [terminal.shop](https://terminal.shop) の制作者によって作られており、ターミナルで可能なことの限界を押し広げます。
- クライアント/サーバー構成。例えば OpenCode をあなたのPCで動かし、モバイルアプリからリモート操作できます。TUI フロントエンドは複数あるクライアントの1つにすぎません。
---
**コミュニティに参加** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

132
README.ko.md Normal file
View File

@@ -0,0 +1,132 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
</picture>
</a>
</p>
<p align="center">오픈 소스 AI 코딩 에이전트.</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### 설치
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# 패키지 매니저
npm i -g opencode-ai@latest # bun/pnpm/yarn 도 가능
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS 및 Linux (권장, 항상 최신)
brew install opencode # macOS 및 Linux (공식 brew formula, 업데이트 빈도 낮음)
paru -S opencode-bin # Arch Linux
mise use -g opencode # 어떤 OS든
nix run nixpkgs#opencode # 또는 github:anomalyco/opencode 로 최신 dev 브랜치
```
> [!TIP]
> 설치 전에 0.1.x 보다 오래된 버전을 제거하세요.
### 데스크톱 앱 (BETA)
OpenCode 는 데스크톱 앱으로도 제공됩니다. [releases page](https://github.com/anomalyco/opencode/releases) 에서 직접 다운로드하거나 [opencode.ai/download](https://opencode.ai/download) 를 이용하세요.
| 플랫폼 | 다운로드 |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, 또는 AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### 설치 디렉터리
설치 스크립트는 설치 경로를 다음 우선순위로 결정합니다.
1. `$OPENCODE_INSTALL_DIR` - 사용자 지정 설치 디렉터리
2. `$XDG_BIN_DIR` - XDG Base Directory Specification 준수 경로
3. `$HOME/bin` - 표준 사용자 바이너리 디렉터리 (존재하거나 생성 가능할 경우)
4. `$HOME/.opencode/bin` - 기본 폴백
```bash
# 예시
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
OpenCode 에는 내장 에이전트 2개가 있으며 `Tab` 키로 전환할 수 있습니다.
- **build** - 기본값, 개발 작업을 위한 전체 권한 에이전트
- **plan** - 분석 및 코드 탐색을 위한 읽기 전용 에이전트
- 기본적으로 파일 편집을 거부
- bash 명령 실행 전에 권한을 요청
- 낯선 코드베이스를 탐색하거나 변경을 계획할 때 적합
또한 복잡한 검색과 여러 단계 작업을 위한 **general** 서브 에이전트가 포함되어 있습니다.
내부적으로 사용되며, 메시지에서 `@general` 로 호출할 수 있습니다.
[agents](https://opencode.ai/docs/agents) 에 대해 더 알아보세요.
### 문서
OpenCode 설정에 대한 자세한 내용은 [**문서**](https://opencode.ai/docs) 를 참고하세요.
### 기여하기
OpenCode 에 기여하고 싶다면, Pull Request 를 제출하기 전에 [contributing docs](./CONTRIBUTING.md) 를 읽어주세요.
### OpenCode 기반으로 만들기
OpenCode 와 관련된 프로젝트를 진행하면서 이름에 "opencode"(예: "opencode-dashboard" 또는 "opencode-mobile") 를 포함한다면, README 에 해당 프로젝트가 OpenCode 팀이 만든 것이 아니며 어떤 방식으로도 우리와 제휴되어 있지 않다는 점을 명시해 주세요.
### FAQ
#### Claude Code 와는 무엇이 다른가요?
기능 면에서는 Claude Code 와 매우 유사합니다. 주요 차이점은 다음과 같습니다.
- 100% 오픈 소스
- 특정 제공자에 묶여 있지 않습니다. [OpenCode Zen](https://opencode.ai/zen) 을 통해 제공하는 모델을 권장하지만, OpenCode 는 Claude, OpenAI, Google 또는 로컬 모델과도 사용할 수 있습니다. 모델이 발전하면서 격차는 줄고 가격은 내려가므로 provider-agnostic 인 것이 중요합니다.
- 기본으로 제공되는 LSP 지원
- TUI 에 집중. OpenCode 는 neovim 사용자와 [terminal.shop](https://terminal.shop) 제작자가 만들었으며, 터미널에서 가능한 것의 한계를 밀어붙입니다.
- 클라이언트/서버 아키텍처. 예를 들어 OpenCode 를 내 컴퓨터에서 실행하면서 모바일 앱으로 원격 조작할 수 있습니다. 즉, TUI 프런트엔드는 가능한 여러 클라이언트 중 하나일 뿐입니다.
---
**커뮤니티에 참여하기** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

View File

@@ -14,6 +14,23 @@
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---

132
README.no.md Normal file
View File

@@ -0,0 +1,132 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
</picture>
</a>
</p>
<p align="center">AI-kodeagent med åpen kildekode.</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Installasjon
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Pakkehåndterere
npm i -g opencode-ai@latest # eller bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS og Linux (anbefalt, alltid oppdatert)
brew install opencode # macOS og Linux (offisiell brew-formel, oppdateres sjeldnere)
paru -S opencode-bin # Arch Linux
mise use -g opencode # alle OS
nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste dev-branch
```
> [!TIP]
> Fjern versjoner eldre enn 0.1.x før du installerer.
### Desktop-app (BETA)
OpenCode er også tilgjengelig som en desktop-app. Last ned direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download).
| Plattform | Nedlasting |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` eller AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### Installasjonsmappe
Installasjonsskriptet bruker følgende prioritet for installasjonsstien:
1. `$OPENCODE_INSTALL_DIR` - Egendefinert installasjonsmappe
2. `$XDG_BIN_DIR` - Sti som følger XDG Base Directory Specification
3. `$HOME/bin` - Standard brukerbinar-mappe (hvis den finnes eller kan opprettes)
4. `$HOME/.opencode/bin` - Standard fallback
```bash
# Eksempler
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
OpenCode har to innebygde agents du kan bytte mellom med `Tab`-tasten.
- **build** - Standard, agent med full tilgang for utviklingsarbeid
- **plan** - Skrivebeskyttet agent for analyse og kodeutforsking
- Nekter filendringer som standard
- Spør om tillatelse før bash-kommandoer
- Ideell for å utforske ukjente kodebaser eller planlegge endringer
Det finnes også en **general**-subagent for komplekse søk og flertrinnsoppgaver.
Den brukes internt og kan kalles via `@general` i meldinger.
Les mer om [agents](https://opencode.ai/docs/agents).
### Dokumentasjon
For mer info om hvordan du konfigurerer OpenCode, [**se dokumentasjonen**](https://opencode.ai/docs).
### Bidra
Hvis du vil bidra til OpenCode, les [contributing docs](./CONTRIBUTING.md) før du sender en pull request.
### Bygge på OpenCode
Hvis du jobber med et prosjekt som er relatert til OpenCode og bruker "opencode" som en del av navnet; for eksempel "opencode-dashboard" eller "opencode-mobile", legg inn en merknad i README som presiserer at det ikke er bygget av OpenCode-teamet og ikke er tilknyttet oss på noen måte.
### FAQ
#### Hvordan er dette forskjellig fra Claude Code?
Det er veldig likt Claude Code når det gjelder funksjonalitet. Her er de viktigste forskjellene:
- 100% open source
- Ikke knyttet til en bestemt leverandør. Selv om vi anbefaler modellene vi tilbyr gjennom [OpenCode Zen](https://opencode.ai/zen); kan OpenCode brukes med Claude, OpenAI, Google eller til og med lokale modeller. Etter hvert som modellene utvikler seg vil gapene lukkes og prisene gå ned, så det er viktig å være provider-agnostic.
- LSP-støtte rett ut av boksen
- Fokus på TUI. OpenCode er bygget av neovim-brukere og skaperne av [terminal.shop](https://terminal.shop); vi kommer til å presse grensene for hva som er mulig i terminalen.
- Klient/server-arkitektur. Dette kan for eksempel la OpenCode kjøre på maskinen din, mens du styrer den eksternt fra en mobilapp. Det betyr at TUI-frontend'en bare er en av de mulige klientene.
---
**Bli med i fellesskapet** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

132
README.pl.md Normal file
View File

@@ -0,0 +1,132 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
</picture>
</a>
</p>
<p align="center">Otwartoźródłowy agent kodujący AI.</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Instalacja
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Menedżery pakietów
npm i -g opencode-ai@latest # albo bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS i Linux (polecane, zawsze aktualne)
brew install opencode # macOS i Linux (oficjalna formuła brew, rzadziej aktualizowana)
paru -S opencode-bin # Arch Linux
mise use -g opencode # dowolny system
nix run nixpkgs#opencode # lub github:anomalyco/opencode dla najnowszej gałęzi dev
```
> [!TIP]
> Przed instalacją usuń wersje starsze niż 0.1.x.
### Aplikacja desktopowa (BETA)
OpenCode jest także dostępny jako aplikacja desktopowa. Pobierz ją bezpośrednio ze strony [releases](https://github.com/anomalyco/opencode/releases) lub z [opencode.ai/download](https://opencode.ai/download).
| Platforma | Pobieranie |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` lub AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### Katalog instalacji
Skrypt instalacyjny stosuje następujący priorytet wyboru ścieżki instalacji:
1. `$OPENCODE_INSTALL_DIR` - Własny katalog instalacji
2. `$XDG_BIN_DIR` - Ścieżka zgodna ze specyfikacją XDG Base Directory
3. `$HOME/bin` - Standardowy katalog binarny użytkownika (jeśli istnieje lub można go utworzyć)
4. `$HOME/.opencode/bin` - Domyślny fallback
```bash
# Przykłady
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
OpenCode zawiera dwóch wbudowanych agentów, między którymi możesz przełączać się klawiszem `Tab`.
- **build** - Domyślny agent z pełnym dostępem do pracy developerskiej
- **plan** - Agent tylko do odczytu do analizy i eksploracji kodu
- Domyślnie odmawia edycji plików
- Pyta o zgodę przed uruchomieniem komend bash
- Idealny do poznawania nieznanych baz kodu lub planowania zmian
Dodatkowo jest subagent **general** do złożonych wyszukiwań i wieloetapowych zadań.
Jest używany wewnętrznie i można go wywołać w wiadomościach przez `@general`.
Dowiedz się więcej o [agents](https://opencode.ai/docs/agents).
### Dokumentacja
Więcej informacji o konfiguracji OpenCode znajdziesz w [**dokumentacji**](https://opencode.ai/docs).
### Współtworzenie
Jeśli chcesz współtworzyć OpenCode, przeczytaj [contributing docs](./CONTRIBUTING.md) przed wysłaniem pull requesta.
### Budowanie na OpenCode
Jeśli pracujesz nad projektem związanym z OpenCode i używasz "opencode" jako części nazwy (na przykład "opencode-dashboard" lub "opencode-mobile"), dodaj proszę notatkę do swojego README, aby wyjaśnić, że projekt nie jest tworzony przez zespół OpenCode i nie jest z nami w żaden sposób powiązany.
### FAQ
#### Czym to się różni od Claude Code?
Jest bardzo podobne do Claude Code pod względem możliwości. Oto kluczowe różnice:
- 100% open source
- Niezależne od dostawcy. Chociaż polecamy modele oferowane przez [OpenCode Zen](https://opencode.ai/zen); OpenCode może być używany z Claude, OpenAI, Google, a nawet z modelami lokalnymi. W miarę jak modele ewoluują, różnice będą się zmniejszać, a ceny spadać, więc ważna jest niezależność od dostawcy.
- Wbudowane wsparcie LSP
- Skupienie na TUI. OpenCode jest budowany przez użytkowników neovim i twórców [terminal.shop](https://terminal.shop); przesuwamy granice tego, co jest możliwe w terminalu.
- Architektura klient/serwer. Pozwala np. uruchomić OpenCode na twoim komputerze, a sterować nim zdalnie z aplikacji mobilnej. To znaczy, że frontend TUI jest tylko jednym z możliwych klientów.
---
**Dołącz do naszej społeczności** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

132
README.ru.md Normal file
View File

@@ -0,0 +1,132 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
</picture>
</a>
</p>
<p align="center">Открытый AI-агент для программирования.</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Установка
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Менеджеры пакетов
npm i -g opencode-ai@latest # или bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS и Linux (рекомендуем, всегда актуально)
brew install opencode # macOS и Linux (официальная формула brew, обновляется реже)
paru -S opencode-bin # Arch Linux
mise use -g opencode # любая ОС
nix run nixpkgs#opencode # или github:anomalyco/opencode для самой свежей ветки dev
```
> [!TIP]
> Перед установкой удалите версии старше 0.1.x.
### Десктопное приложение (BETA)
OpenCode также доступен как десктопное приложение. Скачайте его со [страницы релизов](https://github.com/anomalyco/opencode/releases) или с [opencode.ai/download](https://opencode.ai/download).
| Платформа | Загрузка |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` или AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### Каталог установки
Скрипт установки выбирает путь установки в следующем порядке приоритета:
1. `$OPENCODE_INSTALL_DIR` - Пользовательский каталог установки
2. `$XDG_BIN_DIR` - Путь, совместимый со спецификацией XDG Base Directory
3. `$HOME/bin` - Стандартный каталог пользовательских бинарников (если существует или можно создать)
4. `$HOME/.opencode/bin` - Fallback по умолчанию
```bash
# Примеры
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
В OpenCode есть два встроенных агента, между которыми можно переключаться клавишей `Tab`.
- **build** - По умолчанию, агент с полным доступом для разработки
- **plan** - Агент только для чтения для анализа и изучения кода
- По умолчанию запрещает редактирование файлов
- Запрашивает разрешение перед выполнением bash-команд
- Идеален для изучения незнакомых кодовых баз или планирования изменений
Также включен сабагент **general** для сложных поисков и многошаговых задач.
Он используется внутренне и может быть вызван в сообщениях через `@general`.
Подробнее об [agents](https://opencode.ai/docs/agents).
### Документация
Больше информации о том, как настроить OpenCode: [**наши docs**](https://opencode.ai/docs).
### Вклад
Если вы хотите внести вклад в OpenCode, прочитайте [contributing docs](./CONTRIBUTING.md) перед тем, как отправлять pull request.
### Разработка на базе OpenCode
Если вы делаете проект, связанный с OpenCode, и используете "opencode" как часть имени (например, "opencode-dashboard" или "opencode-mobile"), добавьте примечание в README, чтобы уточнить, что проект не создан командой OpenCode и не аффилирован с нами.
### FAQ
#### Чем это отличается от Claude Code?
По возможностям это очень похоже на Claude Code. Вот ключевые отличия:
- 100% open source
- Не привязано к одному провайдеру. Мы рекомендуем модели из [OpenCode Zen](https://opencode.ai/zen); но OpenCode можно использовать с Claude, OpenAI, Google или даже локальными моделями. По мере развития моделей разрыв будет сокращаться, а цены падать, поэтому важна независимость от провайдера.
- Поддержка LSP из коробки
- Фокус на TUI. OpenCode построен пользователями neovim и создателями [terminal.shop](https://terminal.shop); мы будем раздвигать границы того, что возможно в терминале.
- Архитектура клиент/сервер. Например, это позволяет запускать OpenCode на вашем компьютере, а управлять им удаленно из мобильного приложения. Это значит, что TUI-фронтенд - лишь один из возможных клиентов.
---
**Присоединяйтесь к нашему сообществу** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

View File

@@ -14,6 +14,23 @@
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
@@ -109,10 +126,6 @@ OpenCode 内置两种 Agent可用 `Tab` 键快速切换:
- 聚焦终端界面 (TUI)。OpenCode 由 Neovim 爱好者和 [terminal.shop](https://terminal.shop) 的创建者打造,会持续探索终端的极限。
- 客户端/服务器架构。可在本机运行同时用移动设备远程驱动。TUI 只是众多潜在客户端之一。
#### 另一个同名的仓库是什么?
另一个名字相近的仓库与本项目无关。[点击这里了解背后故事](https://x.com/thdxr/status/1933561254481666466)。
---
**加入我们的社区** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

View File

@@ -14,6 +14,23 @@
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
@@ -31,7 +48,7 @@ choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS 與 Linux推薦始終保持最新
brew install opencode # macOS 與 Linux官方 brew formula更新頻率較低
paru -S opencode-bin # Arch Linux
mise use -g github:anomalyco/opencode # 任何作業系統
mise use -g opencode # 任何作業系統
nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取得最新開發分支
```
@@ -109,10 +126,6 @@ OpenCode 內建了兩種 Agent您可以使用 `Tab` 鍵快速切換。
- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造;我們將不斷挑戰終端機介面的極限。
- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。
#### 另一個同名的 Repo 是什麼?
另一個名稱相近的儲存庫與本專案無關。您可以點此[閱讀背後的故事](https://x.com/thdxr/status/1933561254481666466)。
---
**加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

View File

@@ -207,3 +207,7 @@
| 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) |
| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) |
| 2026-01-21 | 5,444,842 (+315,843) | 1,962,531 (+58,866) | 7,407,373 (+374,709) |
| 2026-01-22 | 5,766,340 (+321,498) | 2,029,487 (+66,956) | 7,795,827 (+388,454) |
| 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748) | 8,192,471 (+396,644) |
| 2026-01-24 | 6,371,019 (+274,783) | 2,156,870 (+60,635) | 8,527,889 (+335,418) |
| 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) |

View File

@@ -1,71 +0,0 @@
## Style Guide
- Keep things in one function unless composable or reusable
- Avoid unnecessary destructuring. Instead of `const { a, b } = obj`, use `obj.a` and `obj.b` to preserve context
- Avoid `try`/`catch` where possible
- Avoid using the `any` type
- Prefer single word variable names where possible
- Use Bun APIs when possible, like `Bun.file()`
# Avoid let statements
We don't like `let` statements, especially combined with if/else statements.
Prefer `const`.
Good:
```ts
const foo = condition ? 1 : 2
```
Bad:
```ts
let foo
if (condition) foo = 1
else foo = 2
```
# Avoid else statements
Prefer early returns or using an `iife` to avoid else statements.
Good:
```ts
function foo() {
if (condition) return 1
return 2
}
```
Bad:
```ts
function foo() {
if (condition) return 1
else return 2
}
```
# Prefer single word naming
Try your best to find a single word name for your variables, functions, etc.
Only use multiple words if you cannot.
Good:
```ts
const foo = 1
const bar = 2
const baz = 3
```
Bad:
```ts
const fooBar = 1
const barBaz = 2
const bazFoo = 3
```

View File

@@ -23,7 +23,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -73,7 +73,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -107,7 +107,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -134,7 +134,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -158,7 +158,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -182,7 +182,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -211,7 +211,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -240,7 +240,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -256,7 +256,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.1.30",
"version": "1.1.36",
"bin": {
"opencode": "./bin/opencode",
},
@@ -284,7 +284,7 @@
"@ai-sdk/vercel": "1.0.31",
"@ai-sdk/xai": "2.0.51",
"@clack/prompts": "1.0.0-alpha.1",
"@gitlab/gitlab-ai-provider": "3.1.3",
"@gitlab/gitlab-ai-provider": "3.3.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.25.2",
@@ -360,7 +360,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -380,9 +380,9 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.1.30",
"version": "1.1.36",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.4",
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
@@ -391,7 +391,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -404,7 +404,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -423,6 +423,7 @@
"marked": "catalog:",
"marked-katex-extension": "5.1.6",
"marked-shiki": "catalog:",
"morphdom": "2.7.8",
"remeda": "catalog:",
"shiki": "catalog:",
"solid-js": "catalog:",
@@ -445,7 +446,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"zod": "catalog:",
},
@@ -456,7 +457,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.1.30",
"version": "1.1.36",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -922,17 +923,19 @@
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.3", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.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-ikumi4PZN/S0f+j/5rb5dBRtORyT41Pl/tj8vHhnpFtpYcxXsaNv2RvCKBVf2/PovvSz2pYMOcpujIU4MdGfyQ=="],
"@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=="],
"@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=="],
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.2", "", { "dependencies": { "ansi-colors": "4.1.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-88cqrrB2cLXN8nMOHidQTcVOnZsJ5kebEbBefjMCifaUCwTA30ouSSWvTZqrOX4O104zjJyu7M8Gcv/NNYQuaA=="],
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.5", "", { "dependencies": { "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q=="],
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="],
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.90.4", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.2", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-9l++kjcb0ui4JqPlueZ6OZ9zKn6eK/8//Z2jHcIXb5MRwDRgubOOSpTU5llEv3uvWfT10VzcMp99dySWq0AASw=="],
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.90.10", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.5", "@hey-api/json-schema-ref-parser": "1.2.2", "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-o0wlFxuLt1bcyIV/ZH8DQ1wrgODTnUYj/VfCHOOYgXUQlLp9Dm2PjihOz+WYrZLowhqUhSKeJRArOGzvLuOTsg=="],
"@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="],
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
@@ -3102,6 +3105,8 @@
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
"morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -4080,6 +4085,8 @@
"@expressive-code/plugin-shiki/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
"@gitlab/gitlab-ai-provider/openai": ["openai@6.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg=="],
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1768302833,
"narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
"lastModified": 1768393167,
"narHash": "sha256-n2063BRjHde6DqAz2zavhOOiLUwA3qXt7jQYHyETjX8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "61db79b0c6b838d9894923920b612048e1201926",
"rev": "2f594d5af95d4fdac67fba60376ec11e482041cb",
"type": "github"
},
"original": {

View File

@@ -77,6 +77,8 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
"checkout.session.expired",
"charge.refunded",
"invoice.payment_succeeded",
"invoice.payment_failed",
"invoice.payment_action_required",
"customer.created",
"customer.deleted",
"customer.updated",
@@ -101,15 +103,26 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
const zenProduct = new stripe.Product("ZenBlack", {
name: "OpenCode Black",
})
const zenPrice = new stripe.Price("ZenBlackPrice", {
const zenPriceProps = {
product: zenProduct.id,
unitAmount: 20000,
currency: "usd",
recurring: {
interval: "month",
intervalCount: 1,
},
}
const zenPrice200 = new stripe.Price("ZenBlackPrice", { ...zenPriceProps, unitAmount: 20000 })
const zenPrice100 = new stripe.Price("ZenBlack100Price", { ...zenPriceProps, unitAmount: 10000 })
const zenPrice20 = new stripe.Price("ZenBlack20Price", { ...zenPriceProps, unitAmount: 2000 })
const ZEN_BLACK_PRICE = new sst.Linkable("ZEN_BLACK_PRICE", {
properties: {
product: zenProduct.id,
plan200: zenPrice200.id,
plan100: zenPrice100.id,
plan20: zenPrice20.id,
},
})
const ZEN_BLACK_LIMITS = new sst.Secret("ZEN_BLACK_LIMITS")
const ZEN_MODELS = [
new sst.Secret("ZEN_MODELS1"),
@@ -121,7 +134,6 @@ const ZEN_MODELS = [
new sst.Secret("ZEN_MODELS7"),
new sst.Secret("ZEN_MODELS8"),
]
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
@@ -164,7 +176,8 @@ new sst.cloudflare.x.SolidStart("Console", {
EMAILOCTOPUS_API_KEY,
AWS_SES_ACCESS_KEY_ID,
AWS_SES_SECRET_ACCESS_KEY,
ZEN_BLACK,
ZEN_BLACK_PRICE,
ZEN_BLACK_LIMITS,
new sst.Secret("ZEN_SESSION_SECRET"),
...ZEN_MODELS,
...($dev

View File

@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-sH6zUk9G4vC6btPZIR9aiSHX0F4aGyUZB7fKbpDUcpE=",
"aarch64-linux": "sha256-CVpdXnFns34hmGwwlRrrI6Uk6B/jZUxfnH4HC2NanEo=",
"aarch64-darwin": "sha256-khP27Iiq+FAZRlzUy7rGXc2MviZjirFH1ShRyd7q1bY=",
"x86_64-darwin": "sha256-nE2p62Tld64sQVMq7j0YNT5Zwjqp22H997+K8xfi1ag="
"x86_64-linux": "sha256-olTZ+tKugAY3LxizsJMlbK3TW78HZUoM03PigvQLP4A=",
"aarch64-linux": "sha256-xdKDeqMEnYM2+vGySfb8pbcYyo/xMmgxG/ZhPCKaZEg=",
"aarch64-darwin": "sha256-fihCTrHIiUG+py4vuqdr+YshqSKm2/B5onY50b97sPM=",
"x86_64-darwin": "sha256-inlQQPNAOdkmKK6HQAMI2bG/ZFlfwmUQu9a6vm6Q0jQ="
}
}

View File

@@ -0,0 +1,35 @@
import { test, expect } from "./fixtures"
import { modKey } from "./utils"
test("smoke file viewer renders real file content", async ({ page, gotoSession }) => {
await gotoSession()
const sep = process.platform === "win32" ? "\\" : "/"
const file = ["packages", "app", "package.json"].join(sep)
await page.keyboard.press(`${modKey}+P`)
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
const input = dialog.getByRole("textbox").first()
await input.fill(file)
const fileItem = dialog
.locator(
'[data-slot="list-item"][data-key^="file:"][data-key*="packages"][data-key*="app"][data-key$="package.json"]',
)
.first()
await expect(fileItem).toBeVisible()
await fileItem.click()
await expect(dialog).toHaveCount(0)
const tab = page.getByRole("tab", { name: "package.json" })
await expect(tab).toBeVisible()
await tab.click()
const code = page.locator('[data-component="code"]').first()
await expect(code).toBeVisible()
await expect(code.getByText("@opencode-ai/app")).toBeVisible()
})

View File

@@ -0,0 +1,43 @@
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
test("smoke model selection updates prompt footer", async ({ page, gotoSession }) => {
await gotoSession()
await page.locator(promptSelector).click()
await page.keyboard.type("/model")
const command = page.locator('[data-slash-id="model.choose"]')
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
const input = dialog.getByRole("textbox").first()
const selected = dialog.locator('[data-slot="list-item"][data-selected="true"]').first()
await expect(selected).toBeVisible()
const other = dialog.locator('[data-slot="list-item"]:not([data-selected="true"])').first()
const target = (await other.count()) > 0 ? other : selected
const key = await target.getAttribute("data-key")
if (!key) throw new Error("Failed to resolve model key from list item")
const name = (await target.locator("span").first().innerText()).trim()
const model = key.split(":").slice(1).join(":")
await input.fill(model)
const item = dialog.locator(`[data-slot="list-item"][data-key="${key}"]`)
await expect(item).toBeVisible()
await item.click()
await expect(dialog).toHaveCount(0)
const form = page.locator(promptSelector).locator("xpath=ancestor::form[1]")
await expect(form.locator('[data-component="button"]').filter({ hasText: name }).first()).toBeVisible()
})

View File

@@ -0,0 +1,26 @@
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
test("smoke @mention inserts file pill token", async ({ page, gotoSession }) => {
await gotoSession()
await page.locator(promptSelector).click()
const sep = process.platform === "win32" ? "\\" : "/"
const file = ["packages", "app", "package.json"].join(sep)
const filePattern = /packages[\\/]+app[\\/]+\s*package\.json/
await page.keyboard.type(`@${file}`)
const suggestion = page.getByRole("button", { name: filePattern }).first()
await expect(suggestion).toBeVisible()
await suggestion.hover()
await page.keyboard.press("Tab")
const pill = page.locator(`${promptSelector} [data-type="file"]`).first()
await expect(pill).toBeVisible()
await expect(pill).toHaveAttribute("data-path", filePattern)
await page.keyboard.type(" ok")
await expect(page.locator(promptSelector)).toContainText("ok")
})

View File

@@ -0,0 +1,22 @@
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
test("smoke /open opens file picker dialog", async ({ page, gotoSession }) => {
await gotoSession()
await page.locator(promptSelector).click()
await page.keyboard.type("/open")
const command = page.locator('[data-slash-id="file.open"]')
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
await expect(dialog.getByRole("textbox").first()).toBeVisible()
await page.keyboard.press("Escape")
await expect(dialog).toHaveCount(0)
})

View File

@@ -0,0 +1,44 @@
import { test, expect } from "./fixtures"
import { modKey } from "./utils"
test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = page.getByRole("dialog")
await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)
const opened = await dialog
.waitFor({ state: "visible", timeout: 3000 })
.then(() => true)
.catch(() => false)
if (!opened) {
await page.getByRole("button", { name: "Settings" }).first().click()
await expect(dialog).toBeVisible()
}
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
await expect(dialog.getByRole("button", { name: "Reset to defaults" })).toBeVisible()
await expect(dialog.getByPlaceholder("Search shortcuts")).toBeVisible()
await page.keyboard.press("Escape")
const closed = await dialog
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (closed) return
await page.keyboard.press("Escape")
const closedSecond = await dialog
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (closedSecond) return
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
await expect(dialog).toHaveCount(0)
})

View File

@@ -0,0 +1,25 @@
import { test, expect } from "./fixtures"
import { promptSelector, terminalSelector, terminalToggleKey } from "./utils"
test("smoke terminal mounts and can create a second tab", async ({ page, gotoSession }) => {
await gotoSession()
const terminals = page.locator(terminalSelector)
const opened = await terminals.first().isVisible()
if (!opened) {
await page.keyboard.press(terminalToggleKey)
}
await expect(terminals.first()).toBeVisible()
await expect(terminals.first().locator("textarea")).toHaveCount(1)
await expect(terminals).toHaveCount(1)
// Ghostty captures a lot of keybinds when focused; move focus back
// to the app shell before triggering `terminal.new`.
await page.locator(promptSelector).click()
await page.keyboard.press("Control+Alt+T")
await expect(terminals).toHaveCount(2)
await expect(terminals.nth(1).locator("textarea")).toHaveCount(1)
})

View File

@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"types": ["node"]
},
"include": ["./**/*.ts"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.1.30",
"version": "1.1.36",
"description": "",
"type": "module",
"exports": {

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -45,7 +45,6 @@ async function waitForHealth(url: string) {
const appDir = process.cwd()
const repoDir = path.resolve(appDir, "../..")
const opencodeDir = path.join(repoDir, "packages", "opencode")
const modelsJson = path.join(opencodeDir, "test", "tool", "fixtures", "models-api.json")
const extraArgs = (() => {
const args = process.argv.slice(2)
@@ -59,8 +58,6 @@ const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-"))
const serverEnv = {
...process.env,
MODELS_DEV_API_JSON: modelsJson,
OPENCODE_DISABLE_MODELS_FETCH: "true",
OPENCODE_DISABLE_SHARE: "true",
OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",

View File

@@ -19,10 +19,12 @@ import { SettingsProvider } from "@/context/settings"
import { TerminalProvider } from "@/context/terminal"
import { PromptProvider } from "@/context/prompt"
import { FileProvider } from "@/context/file"
import { CommentsProvider } from "@/context/comments"
import { NotificationProvider } from "@/context/notification"
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import { CommandProvider } from "@/context/command"
import { LanguageProvider, useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { Logo } from "@opencode-ai/ui/logo"
import Layout from "@/pages/layout"
import DirectoryLayout from "@/pages/directory-layout"
@@ -45,6 +47,11 @@ declare global {
}
}
function MarkedProviderWithNativeParser(props: ParentProps) {
const platform = usePlatform()
return <MarkedProvider nativeParser={platform.parseMarkdown}>{props.children}</MarkedProvider>
}
export function AppBaseProviders(props: ParentProps) {
return (
<MetaProvider>
@@ -54,11 +61,11 @@ export function AppBaseProviders(props: ParentProps) {
<UiI18nBridge>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<DialogProvider>
<MarkedProvider>
<MarkedProviderWithNativeParser>
<DiffComponentProvider component={Diff}>
<CodeComponentProvider component={Code}>{props.children}</CodeComponentProvider>
</DiffComponentProvider>
</MarkedProvider>
</MarkedProviderWithNativeParser>
</DialogProvider>
</ErrorBoundary>
</UiI18nBridge>
@@ -120,13 +127,15 @@ export function AppInterface(props: { defaultUrl?: string }) {
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id ?? "new"} keyed>
<Show when={p.params.id ?? "new"}>
<TerminalProvider>
<FileProvider>
<PromptProvider>
<Suspense fallback={<Loading />}>
<Session />
</Suspense>
<CommentsProvider>
<Suspense fallback={<Loading />}>
<Session />
</Suspense>
</CommentsProvider>
</PromptProvider>
</FileProvider>
</TerminalProvider>

View File

@@ -143,7 +143,17 @@ export function DialogConnectProvider(props: { provider: string }) {
}
return (
<Dialog title={<IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} />}>
<Dialog
title={
<IconButton
tabIndex={-1}
icon="arrow-left"
variant="ghost"
onClick={goBack}
aria-label={language.t("common.goBack")}
/>
}
>
<div class="flex flex-col gap-6 px-2.5 pb-3">
<div class="px-2.5 flex gap-4 items-center">
<ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" />
@@ -177,7 +187,7 @@ export function DialogConnectProvider(props: { provider: string }) {
{(i) => (
<div class="w-full flex items-center gap-x-2">
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
<div class="w-2.5 h-0.5 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
<div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
</div>
<span>{methodLabel(i)}</span>
</div>
@@ -363,6 +373,9 @@ export function DialogConnectProvider(props: { provider: string }) {
})
onMount(async () => {
if (store.authorization?.url) {
platform.openLink(store.authorization.url)
}
const result = await globalSDK.client.provider.oauth
.callback({
providerID: props.provider,

View File

@@ -6,6 +6,7 @@ import { Icon } from "@opencode-ai/ui/icon"
import { createMemo, createSignal, For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { type LocalProject, getAvatarColors } from "@/context/layout"
import { getFilename } from "@opencode-ai/util/path"
import { Avatar } from "@opencode-ai/ui/avatar"
@@ -16,6 +17,7 @@ const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] a
export function DialogEditProject(props: { project: LocalProject }) {
const dialog = useDialog()
const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
const language = useLanguage()
const folderName = createMemo(() => getFilename(props.project.worktree))
@@ -25,6 +27,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
name: defaultName(),
color: props.project.icon?.color || "pink",
iconUrl: props.project.icon?.override || "",
startup: props.project.commands?.start ?? "",
saving: false,
})
@@ -69,15 +72,29 @@ export function DialogEditProject(props: { project: LocalProject }) {
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
if (!props.project.id) return
setStore("saving", true)
const name = store.name.trim() === folderName() ? "" : store.name.trim()
await globalSDK.client.project.update({
projectID: props.project.id,
directory: props.project.worktree,
const start = store.startup.trim()
if (props.project.id && props.project.id !== "global") {
await globalSDK.client.project.update({
projectID: props.project.id,
directory: props.project.worktree,
name,
icon: { color: store.color, override: store.iconUrl },
commands: { start },
})
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
setStore("saving", false)
dialog.close()
return
}
globalSync.project.meta(props.project.worktree, {
name,
icon: { color: store.color, override: store.iconUrl },
icon: { color: store.color, override: store.iconUrl || undefined },
commands: { start: start || undefined },
})
setStore("saving", false)
dialog.close()
@@ -193,6 +210,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
{(color) => (
<button
type="button"
aria-label={language.t("dialog.project.edit.color.select", { color })}
aria-pressed={store.color === color}
classList={{
"flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
@@ -213,6 +232,17 @@ export function DialogEditProject(props: { project: LocalProject }) {
</div>
</div>
</Show>
<TextField
multiline
label={language.t("dialog.project.edit.worktree.startup")}
description={language.t("dialog.project.edit.worktree.startup.description")}
placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
value={store.startup}
onChange={(v) => setStore("startup", v)}
spellcheck={false}
class="max-h-40 w-full font-mono text-xs no-scrollbar"
/>
</div>
<div class="flex justify-end gap-2">

View File

@@ -0,0 +1,310 @@
import { createSignal, createEffect, onMount, onCleanup } from "solid-js"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { markReleaseNotesSeen } from "@/lib/release-notes"
const CHANGELOG_URL = "https://opencode.ai/changelog.json"
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}
function getText(value: unknown): string | undefined {
if (typeof value === "string") {
const text = value.trim()
return text.length > 0 ? text : undefined
}
if (!Array.isArray(value)) return
const parts = value.map((item) => (typeof item === "string" ? item.trim() : "")).filter((item) => item.length > 0)
if (parts.length === 0) return
return parts.join(" ")
}
function normalizeRemoteUrl(url: string): string {
if (url.startsWith("https://") || url.startsWith("http://")) return url
if (url.startsWith("/")) return `https://opencode.ai${url}`
return `https://opencode.ai/${url}`
}
function parseMedia(value: unknown): ReleaseFeature["media"] | undefined {
if (!isRecord(value)) return
const type = getText(value.type)?.toLowerCase()
const src = getText(value.src)
if (!src) return
if (type !== "image" && type !== "video") return
return {
type,
src: normalizeRemoteUrl(src),
alt: getText(value.alt),
}
}
function parseFeature(value: unknown): ReleaseFeature | undefined {
if (!isRecord(value)) return
const title = getText(value.title) ?? getText(value.name) ?? getText(value.heading)
const description = getText(value.description) ?? getText(value.body) ?? getText(value.text)
if (!title) return
if (!description) return
const tag = getText(value.tag) ?? getText(value.label) ?? "New"
const media = (() => {
const parsed = parseMedia(value.media)
if (parsed) return parsed
const alt = getText(value.alt)
const image = getText(value.image)
if (image) return { type: "image" as const, src: normalizeRemoteUrl(image), alt }
const video = getText(value.video)
if (video) return { type: "video" as const, src: normalizeRemoteUrl(video), alt }
})()
return { title, description, tag, media }
}
function parseChangelog(value: unknown): ReleaseNote | undefined {
const releases = (() => {
if (Array.isArray(value)) return value
if (!isRecord(value)) return
if (Array.isArray(value.releases)) return value.releases
if (Array.isArray(value.versions)) return value.versions
if (Array.isArray(value.changelog)) return value.changelog
})()
if (!releases) {
if (!isRecord(value)) return
if (!Array.isArray(value.highlights)) return
const features = value.highlights.map(parseFeature).filter((item): item is ReleaseFeature => item !== undefined)
if (features.length === 0) return
return { version: CURRENT_RELEASE.version, features: features.slice(0, 3) }
}
const version = (() => {
const head = releases[0]
if (!isRecord(head)) return
return getText(head.version) ?? getText(head.tag_name) ?? getText(head.tag) ?? getText(head.name)
})()
const features = releases
.flatMap((item) => {
if (!isRecord(item)) return []
const highlights = item.highlights
if (!Array.isArray(highlights)) return []
return highlights.map(parseFeature).filter((feature): feature is ReleaseFeature => feature !== undefined)
})
.slice(0, 3)
if (features.length === 0) return
return { version: version ?? CURRENT_RELEASE.version, features }
}
export interface ReleaseFeature {
title: string
description: string
tag?: string
media?: {
type: "image" | "video"
src: string
alt?: string
}
}
export interface ReleaseNote {
version: string
features: ReleaseFeature[]
}
// Current release notes - update this with each release
export const CURRENT_RELEASE: ReleaseNote = {
version: "1.0.0",
features: [
{
title: "Cleaner tab experience",
description: "Chat is now fixed to the side of your tabs, and review is now available as a dedicated tab. ",
tag: "New",
media: {
type: "video",
src: "/release/release-example.mp4",
alt: "Cleaner tab experience",
},
},
{
title: "Share with control",
description: "Keep your sessions private by default, or publish them to the web with a shareable URL.",
tag: "New",
media: {
type: "image",
src: "/release/release-share.png",
alt: "Share with control",
},
},
{
title: "Improved attachment management",
description: "Upload and manage attachments more easily, to help build and maintain context.",
tag: "New",
media: {
type: "video",
src: "/release/release-example.mp4",
alt: "Improved attachment management",
},
},
],
}
export function DialogReleaseNotes(props: { release?: ReleaseNote }) {
const dialog = useDialog()
const [note, setNote] = createSignal(props.release ?? CURRENT_RELEASE)
const [index, setIndex] = createSignal(0)
const feature = () => note().features[index()] ?? note().features[0] ?? CURRENT_RELEASE.features[0]!
const total = () => note().features.length
const isFirst = () => index() === 0
const isLast = () => index() === total() - 1
function handleNext() {
if (!isLast()) setIndex(index() + 1)
}
function handleBack() {
if (!isFirst()) setIndex(index() - 1)
}
function handleClose() {
markReleaseNotesSeen()
dialog.close()
}
let focusTrap: HTMLDivElement | undefined
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "ArrowLeft" && !isFirst()) {
e.preventDefault()
setIndex(index() - 1)
}
if (e.key === "ArrowRight" && !isLast()) {
e.preventDefault()
setIndex(index() + 1)
}
}
onMount(() => {
focusTrap?.focus()
document.addEventListener("keydown", handleKeyDown)
onCleanup(() => document.removeEventListener("keydown", handleKeyDown))
const controller = new AbortController()
fetch(CHANGELOG_URL, {
signal: controller.signal,
headers: { Accept: "application/json" },
})
.then((response) => (response.ok ? (response.json() as Promise<unknown>) : undefined))
.then((json) => {
if (!json) return
const parsed = parseChangelog(json)
if (!parsed) return
setNote({
version: parsed.version,
features: parsed.features,
})
setIndex(0)
})
.catch(() => undefined)
onCleanup(() => controller.abort())
})
// Refocus the trap when index changes to ensure escape always works
createEffect(() => {
index() // track index
focusTrap?.focus()
})
return (
<Dialog class="dialog-release-notes">
{/* Hidden element to capture initial focus and handle escape */}
<div ref={focusTrap} tabindex="0" class="absolute opacity-0 pointer-events-none" />
{/* Left side - Text content */}
<div class="flex flex-col flex-1 min-w-0 p-8">
{/* Top section - feature content (fixed position from top) */}
<div class="flex flex-col gap-2 pt-22">
<div class="flex items-center gap-2">
<h1 class="text-16-medium text-text-strong">{feature().title}</h1>
{feature().tag && (
<span
class="text-12-medium text-text-weak px-1.5 py-0.5 bg-surface-base rounded-sm border border-border-weak-base"
style={{ "border-width": "0.5px" }}
>
{feature().tag}
</span>
)}
</div>
<p class="text-14-regular text-text-base">{feature().description}</p>
</div>
{/* Spacer to push buttons to bottom */}
<div class="flex-1" />
{/* Bottom section - buttons and indicators (fixed position) */}
<div class="flex flex-col gap-12">
<div class="flex items-center gap-3">
{isLast() ? (
<Button variant="primary" size="large" onClick={handleClose}>
Get started
</Button>
) : (
<Button variant="secondary" size="large" onClick={handleNext}>
Next
</Button>
)}
</div>
{total() > 1 && (
<div class="flex items-center gap-1.5 -my-2.5">
{note().features.map((_, i) => (
<button
type="button"
class="h-6 flex items-center cursor-pointer bg-transparent border-none p-0 transition-all duration-200"
classList={{
"w-8": i === index(),
"w-3": i !== index(),
}}
onClick={() => setIndex(i)}
>
<div
class="w-full h-0.5 rounded-[1px] transition-colors duration-200"
classList={{
"bg-icon-strong-base": i === index(),
"bg-icon-weak-base": i !== index(),
}}
/>
</button>
))}
</div>
)}
</div>
</div>
{/* Right side - Media content (edge to edge) */}
{feature().media && (
<div class="flex-1 min-w-0 bg-surface-base overflow-hidden rounded-r-xl">
{feature().media!.type === "image" ? (
<img
src={feature().media!.src}
alt={feature().media!.alt ?? "Release preview"}
class="w-full h-full object-cover"
/>
) : (
<video src={feature().media!.src} autoplay loop muted playsinline class="w-full h-full object-cover" />
)}
</div>
)}
</Dialog>
)
}

View File

@@ -3,6 +3,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { List } from "@opencode-ai/ui/list"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import fuzzysort from "fuzzysort"
import { createMemo } from "solid-js"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
@@ -21,63 +22,153 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
const language = useLanguage()
const home = createMemo(() => sync.data.path.home)
const root = createMemo(() => sync.data.path.home || sync.data.path.directory)
const start = createMemo(() => sync.data.path.home || sync.data.path.directory)
const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>()
function normalize(input: string) {
const v = input.replaceAll("\\", "/")
if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/")
return v.replace(/\/+/g, "/")
}
function normalizeDriveRoot(input: string) {
const v = normalize(input)
if (/^[A-Za-z]:$/.test(v)) return v + "/"
return v
}
function trimTrailing(input: string) {
const v = normalizeDriveRoot(input)
if (v === "/") return v
if (v === "//") return v
if (/^[A-Za-z]:\/$/.test(v)) return v
return v.replace(/\/+$/, "")
}
function join(base: string | undefined, rel: string) {
const b = (base ?? "").replace(/[\\/]+$/, "")
const r = rel.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")
const b = trimTrailing(base ?? "")
const r = trimTrailing(rel).replace(/^\/+/, "")
if (!b) return r
if (!r) return b
if (b.endsWith("/")) return b + r
return b + "/" + r
}
function display(rel: string) {
const full = join(root(), rel)
function rootOf(input: string) {
const v = normalizeDriveRoot(input)
if (v.startsWith("//")) return "//"
if (v.startsWith("/")) return "/"
if (/^[A-Za-z]:\//.test(v)) return v.slice(0, 3)
return ""
}
function display(path: string) {
const full = trimTrailing(path)
const h = home()
if (!h) return full
if (full === h) return "~"
if (full.startsWith(h + "/") || full.startsWith(h + "\\")) {
return "~" + full.slice(h.length)
}
const hn = trimTrailing(h)
const lc = full.toLowerCase()
const hc = hn.toLowerCase()
if (lc === hc) return "~"
if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length)
return full
}
function normalizeQuery(query: string) {
function scoped(filter: string) {
const base = start()
if (!base) return
const raw = normalizeDriveRoot(filter.trim())
if (!raw) return { directory: trimTrailing(base), path: "" }
const h = home()
if (raw === "~") return { directory: trimTrailing(h ?? base), path: "" }
if (raw.startsWith("~/")) return { directory: trimTrailing(h ?? base), path: raw.slice(2) }
if (!query) return query
if (query.startsWith("~/")) return query.slice(2)
if (h) {
const lc = query.toLowerCase()
const hc = h.toLowerCase()
if (lc === hc || lc.startsWith(hc + "/") || lc.startsWith(hc + "\\")) {
return query.slice(h.length).replace(/^[\\/]+/, "")
}
}
return query
const root = rootOf(raw)
if (root) return { directory: trimTrailing(root), path: raw.slice(root.length) }
return { directory: trimTrailing(base), path: raw }
}
async function fetchDirs(query: string) {
const directory = root()
if (!directory) return [] as string[]
async function dirs(dir: string) {
const key = trimTrailing(dir)
const existing = cache.get(key)
if (existing) return existing
const results = await sdk.client.find
.files({ directory, query, type: "directory", limit: 50 })
const request = sdk.client.file
.list({ directory: key, path: "" })
.then((x) => x.data ?? [])
.catch(() => [])
.then((nodes) =>
nodes
.filter((n) => n.type === "directory")
.map((n) => ({
name: n.name,
absolute: trimTrailing(normalizeDriveRoot(n.absolute)),
})),
)
return results.map((x) => x.replace(/[\\/]+$/, ""))
cache.set(key, request)
return request
}
async function match(dir: string, query: string, limit: number) {
const items = await dirs(dir)
if (!query) return items.slice(0, limit).map((x) => x.absolute)
return fuzzysort.go(query, items, { key: "name", limit }).map((x) => x.obj.absolute)
}
const directories = async (filter: string) => {
const query = normalizeQuery(filter.trim())
return fetchDirs(query)
const input = scoped(filter)
if (!input) return [] as string[]
const raw = normalizeDriveRoot(filter.trim())
const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/")
const query = normalizeDriveRoot(input.path)
if (!isPath) {
const results = await sdk.client.find
.files({ directory: input.directory, query, type: "directory", limit: 50 })
.then((x) => x.data ?? [])
.catch(() => [])
return results.map((rel) => join(input.directory, rel)).slice(0, 50)
}
const segments = query.replace(/^\/+/, "").split("/")
const head = segments.slice(0, segments.length - 1).filter((x) => x && x !== ".")
const tail = segments[segments.length - 1] ?? ""
const cap = 12
const branch = 4
let paths = [input.directory]
for (const part of head) {
if (part === "..") {
paths = paths.map((p) => {
const v = trimTrailing(p)
if (v === "/") return v
if (/^[A-Za-z]:\/$/.test(v)) return v
const i = v.lastIndexOf("/")
if (i <= 0) return "/"
return v.slice(0, i)
})
continue
}
const next = (await Promise.all(paths.map((p) => match(p, part, branch)))).flat()
paths = Array.from(new Set(next)).slice(0, cap)
if (paths.length === 0) return [] as string[]
}
const out = (await Promise.all(paths.map((p) => match(p, tail, 50)))).flat()
return Array.from(new Set(out)).slice(0, 50)
}
function resolve(rel: string) {
const absolute = join(root(), rel)
function resolve(absolute: string) {
props.onSelect(props.multiple ? [absolute] : absolute)
dialog.close()
}
@@ -95,12 +186,12 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
resolve(path)
}}
>
{(rel) => {
const path = display(rel)
{(absolute) => {
const path = display(absolute)
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: rel, type: "directory" }} class="shrink-0 size-4" />
<FileIcon node={{ path: absolute, type: "directory" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular min-w-0">
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(path)}

View File

@@ -32,8 +32,8 @@ export function DialogSelectFile() {
const dialog = useDialog()
const params = useParams()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey()))
const view = createMemo(() => layout.view(sessionKey()))
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
const state = { cleanup: undefined as (() => void) | void, committed: false }
const [grouped, setGrouped] = createSignal(false)
const common = [

View File

@@ -35,9 +35,10 @@ export const DialogSelectModelUnpaid: Component = () => {
return (
<Dialog title={language.t("dialog.model.select.title")}>
<div class="flex flex-col gap-3 px-2.5">
<div class="flex flex-col gap-3 px-2.5 flex-1 min-h-0">
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
<List
class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
ref={(ref) => (listRef = ref)}
items={local.model.list}
current={local.model.current()}

View File

@@ -1,5 +1,6 @@
import { Popover as Kobalte } from "@kobalte/core/popover"
import { Component, createMemo, createSignal, JSX, Show } from "solid-js"
import { Component, ComponentProps, createEffect, createMemo, JSX, onCleanup, Show, ValidComponent } from "solid-js"
import { createStore } from "solid-js/store"
import { useLocal } from "@/context/local"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { popularProviders } from "@/hooks/use-providers"
@@ -86,28 +87,124 @@ const ModelList: Component<{
)
}
export const ModelSelectorPopover: Component<{
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
provider?: string
children: JSX.Element
}> = (props) => {
const [open, setOpen] = createSignal(false)
children?: JSX.Element
triggerAs?: T
triggerProps?: ComponentProps<T>
}) {
const [store, setStore] = createStore<{
open: boolean
dismiss: "escape" | "outside" | null
trigger?: HTMLElement
content?: HTMLElement
}>({
open: false,
dismiss: null,
trigger: undefined,
content: undefined,
})
const dialog = useDialog()
const handleManage = () => {
setOpen(false)
setStore("open", false)
dialog.show(() => <DialogManageModels />)
}
const language = useLanguage()
createEffect(() => {
if (!store.open) return
const inside = (node: Node | null | undefined) => {
if (!node) return false
const el = store.content
if (el && el.contains(node)) return true
const anchor = store.trigger
if (anchor && anchor.contains(node)) return true
return false
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.key !== "Escape") return
setStore("dismiss", "escape")
setStore("open", false)
event.preventDefault()
event.stopPropagation()
}
const onPointerDown = (event: PointerEvent) => {
const target = event.target
if (!(target instanceof Node)) return
if (inside(target)) return
setStore("dismiss", "outside")
setStore("open", false)
}
const onFocusIn = (event: FocusEvent) => {
if (!store.content) return
const target = event.target
if (!(target instanceof Node)) return
if (inside(target)) return
setStore("dismiss", "outside")
setStore("open", false)
}
window.addEventListener("keydown", onKeyDown, true)
window.addEventListener("pointerdown", onPointerDown, true)
window.addEventListener("focusin", onFocusIn, true)
onCleanup(() => {
window.removeEventListener("keydown", onKeyDown, true)
window.removeEventListener("pointerdown", onPointerDown, true)
window.removeEventListener("focusin", onFocusIn, true)
})
})
return (
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
<Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
<Kobalte
open={store.open}
onOpenChange={(next) => {
if (next) setStore("dismiss", null)
setStore("open", next)
}}
modal={false}
placement="top-start"
gutter={8}
>
<Kobalte.Trigger
ref={(el) => setStore("trigger", el)}
as={props.triggerAs ?? "div"}
{...(props.triggerProps as any)}
>
{props.children}
</Kobalte.Trigger>
<Kobalte.Portal>
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
<Kobalte.Content
ref={(el) => setStore("content", el)}
class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
onEscapeKeyDown={(event) => {
setStore("dismiss", "escape")
setStore("open", false)
event.preventDefault()
event.stopPropagation()
}}
onPointerDownOutside={() => {
setStore("dismiss", "outside")
setStore("open", false)
}}
onFocusOutside={() => {
setStore("dismiss", "outside")
setStore("open", false)
}}
onCloseAutoFocus={(event) => {
if (store.dismiss === "outside") event.preventDefault()
setStore("dismiss", null)
}}
>
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
<ModelList
provider={props.provider}
onSelect={() => setOpen(false)}
onSelect={() => setStore("open", false)}
class="p-1"
action={
<IconButton

View File

@@ -56,6 +56,12 @@ export const DialogSelectProvider: Component = () => {
<Show when={i.id === "anthropic"}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
</Show>
<Show when={i.id === "openai"}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
</Show>
<Show when={i.id.startsWith("github-copilot")}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
</Show>
</div>
)}
</List>

View File

@@ -1,23 +1,48 @@
import { createResource, createEffect, createMemo, onCleanup, Show } from "solid-js"
import { createResource, createEffect, createMemo, onCleanup, Show, createSignal } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { TextField } from "@opencode-ai/ui/text-field"
import { Button } from "@opencode-ai/ui/button"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TextField } from "@opencode-ai/ui/text-field"
import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server"
import { usePlatform } from "@/context/platform"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { useNavigate } from "@solidjs/router"
import { useLanguage } from "@/context/language"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useGlobalSDK } from "@/context/global-sdk"
type ServerStatus = { healthy: boolean; version?: string }
async function checkHealth(url: string, fetch?: typeof globalThis.fetch): Promise<ServerStatus> {
interface AddRowProps {
value: string
placeholder: string
adding: boolean
error: string
status: boolean | undefined
onChange: (value: string) => void
onKeyDown: (event: KeyboardEvent) => void
onBlur: () => void
}
interface EditRowProps {
value: string
placeholder: string
busy: boolean
error: string
status: boolean | undefined
onChange: (value: string) => void
onKeyDown: (event: KeyboardEvent) => void
onBlur: () => void
}
async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>): Promise<ServerStatus> {
const sdk = createOpencodeClient({
baseUrl: url,
fetch,
fetch: platform.fetch,
signal: AbortSignal.timeout(3000),
})
return sdk.global
@@ -26,21 +51,158 @@ async function checkHealth(url: string, fetch?: typeof globalThis.fetch): Promis
.catch(() => ({ healthy: false }))
}
function AddRow(props: AddRowProps) {
return (
<div class="flex items-center px-4 min-h-14 py-3 min-w-0 flex-1">
<div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
<div
classList={{
"size-1.5 rounded-full absolute left-3 z-10 pointer-events-none": true,
"bg-icon-success-base": props.status === true,
"bg-icon-critical-base": props.status === false,
"bg-border-weak-base": props.status === undefined,
}}
style={{ top: "50%", transform: "translateY(-50%)" }}
ref={(el) => {
// Position relative to input-wrapper
requestAnimationFrame(() => {
const wrapper = el.parentElement?.querySelector('[data-slot="input-wrapper"]')
if (wrapper instanceof HTMLElement) {
wrapper.style.position = "relative"
wrapper.appendChild(el)
}
})
}}
/>
<TextField
type="text"
hideLabel
placeholder={props.placeholder}
value={props.value}
autofocus
validationState={props.error ? "invalid" : "valid"}
error={props.error}
disabled={props.adding}
onChange={props.onChange}
onKeyDown={props.onKeyDown}
onBlur={props.onBlur}
class="pl-7"
/>
</div>
</div>
)
}
function EditRow(props: EditRowProps) {
return (
<div class="flex items-center gap-3 px-4 min-w-0 flex-1" onClick={(event) => event.stopPropagation()}>
<div
classList={{
"size-1.5 rounded-full shrink-0": true,
"bg-icon-success-base": props.status === true,
"bg-icon-critical-base": props.status === false,
"bg-border-weak-base": props.status === undefined,
}}
/>
<div class="flex-1 min-w-0">
<TextField
type="text"
hideLabel
placeholder={props.placeholder}
value={props.value}
autofocus
validationState={props.error ? "invalid" : "valid"}
error={props.error}
disabled={props.busy}
onChange={props.onChange}
onKeyDown={props.onKeyDown}
onBlur={props.onBlur}
/>
</div>
</div>
)
}
export function DialogSelectServer() {
const navigate = useNavigate()
const dialog = useDialog()
const server = useServer()
const platform = usePlatform()
const globalSDK = useGlobalSDK()
const language = useLanguage()
const [store, setStore] = createStore({
url: "",
adding: false,
error: "",
status: {} as Record<string, ServerStatus | undefined>,
addServer: {
url: "",
adding: false,
error: "",
showForm: false,
status: undefined as boolean | undefined,
},
editServer: {
id: undefined as string | undefined,
value: "",
error: "",
busy: false,
status: undefined as boolean | undefined,
},
})
const [defaultUrl, defaultUrlActions] = createResource(() => platform.getDefaultServerUrl?.())
const [defaultUrl, defaultUrlActions] = createResource(
async () => {
const url = await platform.getDefaultServerUrl?.()
if (!url) return null
return normalizeServerUrl(url) ?? null
},
{ initialValue: null },
)
const isDesktop = platform.platform === "desktop"
const looksComplete = (value: string) => {
const normalized = normalizeServerUrl(value)
if (!normalized) return false
const host = normalized.replace(/^https?:\/\//, "").split("/")[0]
if (!host) return false
if (host.includes("localhost") || host.startsWith("127.0.0.1")) return true
return host.includes(".") || host.includes(":")
}
const previewStatus = async (value: string, setStatus: (value: boolean | undefined) => void) => {
setStatus(undefined)
if (!looksComplete(value)) return
const normalized = normalizeServerUrl(value)
if (!normalized) return
const result = await checkHealth(normalized, platform)
setStatus(result.healthy)
}
const resetAdd = () => {
setStore("addServer", {
url: "",
error: "",
showForm: false,
status: undefined,
})
}
const resetEdit = () => {
setStore("editServer", {
id: undefined,
value: "",
error: "",
status: undefined,
busy: false,
})
}
const replaceServer = (original: string, next: string) => {
const active = server.url
const nextActive = active === original ? next : active
server.add(next)
if (nextActive) server.setActive(nextActive)
server.remove(original)
}
const items = createMemo(() => {
const current = server.url
const list = server.list
@@ -74,7 +236,7 @@ export function DialogSelectServer() {
const results: Record<string, ServerStatus> = {}
await Promise.all(
items().map(async (url) => {
results[url] = await checkHealth(url, platform.fetch)
results[url] = await checkHealth(url, platform)
}),
)
setStore("status", reconcile(results))
@@ -87,7 +249,7 @@ export function DialogSelectServer() {
onCleanup(() => clearInterval(interval))
})
function select(value: string, persist?: boolean) {
async function select(value: string, persist?: boolean) {
if (!persist && store.status[value]?.healthy === false) return
dialog.close()
if (persist) {
@@ -99,24 +261,101 @@ export function DialogSelectServer() {
navigate("/")
}
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
const value = normalizeServerUrl(store.url)
if (!value) return
const handleAddChange = (value: string) => {
if (store.addServer.adding) return
setStore("addServer", { url: value, error: "" })
void previewStatus(value, (next) => setStore("addServer", { status: next }))
}
setStore("adding", true)
setStore("error", "")
const scrollListToBottom = () => {
const scroll = document.querySelector<HTMLDivElement>('[data-component="list"] [data-slot="list-scroll"]')
if (!scroll) return
requestAnimationFrame(() => {
scroll.scrollTop = scroll.scrollHeight
})
}
const result = await checkHealth(value, platform.fetch)
setStore("adding", false)
const handleEditChange = (value: string) => {
if (store.editServer.busy) return
setStore("editServer", { value, error: "" })
void previewStatus(value, (next) => setStore("editServer", { status: next }))
}
if (!result.healthy) {
setStore("error", language.t("dialog.server.add.error"))
async function handleAdd(value: string) {
if (store.addServer.adding) return
const normalized = normalizeServerUrl(value)
if (!normalized) {
resetAdd()
return
}
setStore("url", "")
select(value, true)
setStore("addServer", { adding: true, error: "" })
const result = await checkHealth(normalized, platform)
setStore("addServer", { adding: false })
if (!result.healthy) {
setStore("addServer", { error: language.t("dialog.server.add.error") })
return
}
resetAdd()
await select(normalized, true)
}
async function handleEdit(original: string, value: string) {
if (store.editServer.busy) return
const normalized = normalizeServerUrl(value)
if (!normalized) {
resetEdit()
return
}
if (normalized === original) {
resetEdit()
return
}
setStore("editServer", { busy: true, error: "" })
const result = await checkHealth(normalized, platform)
setStore("editServer", { busy: false })
if (!result.healthy) {
setStore("editServer", { error: language.t("dialog.server.add.error") })
return
}
replaceServer(original, normalized)
resetEdit()
}
const handleAddKey = (event: KeyboardEvent) => {
event.stopPropagation()
if (event.key !== "Enter" || event.isComposing) return
event.preventDefault()
handleAdd(store.addServer.url)
}
const blurAdd = () => {
if (!store.addServer.url.trim()) {
resetAdd()
return
}
handleAdd(store.addServer.url)
}
const handleEditKey = (event: KeyboardEvent, original: string) => {
event.stopPropagation()
if (event.key === "Escape") {
event.preventDefault()
resetEdit()
return
}
if (event.key !== "Enter" || event.isComposing) return
event.preventDefault()
handleEdit(original, store.editServer.value)
}
async function handleRemove(url: string) {
@@ -124,124 +363,203 @@ export function DialogSelectServer() {
}
return (
<Dialog title={language.t("dialog.server.title")} description={language.t("dialog.server.description")}>
<div class="flex flex-col gap-4 pb-4">
<Dialog title={language.t("dialog.server.title")}>
<div class="flex flex-col gap-2">
<List
search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: true }}
search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: false }}
noInitialSelection
emptyMessage={language.t("dialog.server.empty")}
items={sortedItems}
key={(x) => x}
current={current()}
onSelect={(x) => {
if (x) select(x)
}}
onFilter={(value) => {
if (value && store.addServer.showForm && !store.addServer.adding) {
resetAdd()
}
}}
divider={true}
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent [&_[data-slot=list-item-add]]:px-0"
add={
store.addServer.showForm
? {
render: () => (
<AddRow
value={store.addServer.url}
placeholder={language.t("dialog.server.add.placeholder")}
adding={store.addServer.adding}
error={store.addServer.error}
status={store.addServer.status}
onChange={handleAddChange}
onKeyDown={handleAddKey}
onBlur={blurAdd}
/>
),
}
: undefined
}
>
{(i) => (
<div class="flex items-center gap-2 min-w-0 flex-1 group/item">
<div
class="flex items-center gap-2 min-w-0 flex-1"
classList={{ "opacity-50": store.status[i]?.healthy === false }}
>
<div
classList={{
"size-1.5 rounded-full shrink-0": true,
"bg-icon-success-base": store.status[i]?.healthy === true,
"bg-icon-critical-base": store.status[i]?.healthy === false,
"bg-border-weak-base": store.status[i] === undefined,
}}
/>
<span class="truncate">{serverDisplayName(i)}</span>
<span class="text-text-weak">{store.status[i]?.version}</span>
{(i) => {
const [truncated, setTruncated] = createSignal(false)
let nameRef: HTMLSpanElement | undefined
let versionRef: HTMLSpanElement | undefined
const check = () => {
const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
const versionTruncated = versionRef ? versionRef.scrollWidth > versionRef.clientWidth : false
setTruncated(nameTruncated || versionTruncated)
}
createEffect(() => {
check()
window.addEventListener("resize", check)
onCleanup(() => window.removeEventListener("resize", check))
})
const tooltipValue = () => {
const name = serverDisplayName(i)
const version = store.status[i]?.version
return (
<span class="flex items-center gap-2">
<span>{name}</span>
<Show when={version}>
<span class="text-text-invert-base">{version}</span>
</Show>
</span>
)
}
return (
<div class="flex items-center gap-3 min-w-0 flex-1 group/item">
<Show
when={store.editServer.id !== i}
fallback={
<EditRow
value={store.editServer.value}
placeholder={language.t("dialog.server.add.placeholder")}
busy={store.editServer.busy}
error={store.editServer.error}
status={store.editServer.status}
onChange={handleEditChange}
onKeyDown={(event) => handleEditKey(event, i)}
onBlur={() => handleEdit(i, store.editServer.value)}
/>
}
>
<Tooltip value={tooltipValue()} placement="top" inactive={!truncated()}>
<div
class="flex items-center gap-3 px-4 min-w-0 flex-1"
classList={{ "opacity-50": store.status[i]?.healthy === false }}
>
<div
classList={{
"size-1.5 rounded-full shrink-0": true,
"bg-icon-success-base": store.status[i]?.healthy === true,
"bg-icon-critical-base": store.status[i]?.healthy === false,
"bg-border-weak-base": store.status[i] === undefined,
}}
/>
<span ref={nameRef} class="truncate">
{serverDisplayName(i)}
</span>
<Show when={store.status[i]?.version}>
<span ref={versionRef} class="text-text-weak text-14-regular truncate">
{store.status[i]?.version}
</span>
</Show>
<Show when={defaultUrl() === i}>
<span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
{language.t("dialog.server.status.default")}
</span>
</Show>
</div>
</Tooltip>
</Show>
<Show when={store.editServer.id !== i}>
<div class="flex items-center justify-center gap-5 pl-4">
<Show when={current() === i}>
<p class="text-text-weak text-12-regular">{language.t("dialog.server.current")}</p>
</Show>
<DropdownMenu>
<DropdownMenu.Trigger
as={IconButton}
icon="dot-grid"
variant="ghost"
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
onClick={(e: MouseEvent) => e.stopPropagation()}
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content class="mt-1">
<DropdownMenu.Item
onSelect={() => {
setStore("editServer", {
id: i,
value: i,
error: "",
status: store.status[i]?.healthy,
})
}}
>
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<Show when={isDesktop && defaultUrl() !== i}>
<DropdownMenu.Item
onSelect={async () => {
await platform.setDefaultServerUrl?.(i)
defaultUrlActions.mutate(i)
}}
>
<DropdownMenu.ItemLabel>
{language.t("dialog.server.menu.default")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</Show>
<Show when={isDesktop && defaultUrl() === i}>
<DropdownMenu.Item
onSelect={async () => {
await platform.setDefaultServerUrl?.(null)
defaultUrlActions.mutate(null)
}}
>
<DropdownMenu.ItemLabel>
{language.t("dialog.server.menu.defaultRemove")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</Show>
<DropdownMenu.Separator />
<DropdownMenu.Item
onSelect={() => handleRemove(i)}
class="text-text-on-critical-base hover:bg-surface-critical-weak"
>
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
</Show>
</div>
<Show when={current() !== i && server.list.includes(i)}>
<IconButton
icon="circle-x"
variant="ghost"
class="bg-transparent transition-opacity shrink-0 hover:scale-110"
onClick={(e) => {
e.stopPropagation()
handleRemove(i)
}}
/>
</Show>
</div>
)}
)
}}
</List>
<div class="mt-6 px-3 flex flex-col gap-1.5">
<div class="px-3">
<h3 class="text-14-regular text-text-weak">{language.t("dialog.server.add.title")}</h3>
</div>
<form onSubmit={handleSubmit}>
<div class="flex items-start gap-2">
<div class="flex-1 min-w-0 h-auto">
<TextField
type="text"
label={language.t("dialog.server.add.url")}
hideLabel
placeholder={language.t("dialog.server.add.placeholder")}
value={store.url}
onChange={(v) => {
setStore("url", v)
setStore("error", "")
}}
validationState={store.error ? "invalid" : "valid"}
error={store.error}
/>
</div>
<Button type="submit" variant="secondary" icon="plus-small" size="large" disabled={store.adding}>
{store.adding ? language.t("dialog.server.add.checking") : language.t("dialog.server.add.button")}
</Button>
</div>
</form>
<div class="px-5 pb-5">
<Button
variant="secondary"
icon="plus-small"
size="large"
onClick={() => {
setStore("addServer", { showForm: true, url: "", error: "" })
scrollListToBottom()
}}
class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
>
{store.addServer.adding ? language.t("dialog.server.add.checking") : language.t("dialog.server.add.button")}
</Button>
</div>
<Show when={isDesktop}>
<div class="mt-6 px-3 flex flex-col gap-1.5">
<div class="px-3">
<h3 class="text-14-regular text-text-weak">{language.t("dialog.server.default.title")}</h3>
<p class="text-12-regular text-text-weak mt-1">{language.t("dialog.server.default.description")}</p>
</div>
<div class="flex items-center gap-2 px-3 py-2">
<Show
when={defaultUrl()}
fallback={
<Show
when={server.url}
fallback={
<span class="text-14-regular text-text-weak">{language.t("dialog.server.default.none")}</span>
}
>
<Button
variant="secondary"
size="small"
onClick={async () => {
await platform.setDefaultServerUrl?.(server.url)
defaultUrlActions.refetch(server.url)
}}
>
{language.t("dialog.server.default.set")}
</Button>
</Show>
}
>
<div class="flex items-center gap-2 flex-1 min-w-0">
<span class="truncate text-14-regular">{serverDisplayName(defaultUrl()!)}</span>
</div>
<Button
variant="ghost"
size="small"
onClick={async () => {
await platform.setDefaultServerUrl?.(null)
defaultUrlActions.refetch()
}}
>
{language.t("dialog.server.default.clear")}
</Button>
</Show>
</div>
</div>
</Show>
</div>
</Dialog>
)

View File

@@ -3,6 +3,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Icon } from "@opencode-ai/ui/icon"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { SettingsGeneral } from "./settings-general"
import { SettingsKeybinds } from "./settings-keybinds"
import { SettingsPermissions } from "./settings-permissions"
@@ -14,58 +15,45 @@ import { SettingsMcp } from "./settings-mcp"
export const DialogSettings: Component = () => {
const language = useLanguage()
const platform = usePlatform()
return (
<Dialog size="x-large">
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
<Tabs.List>
<div
style={{
display: "flex",
"flex-direction": "column",
gap: "12px",
width: "100%",
"padding-top": "12px",
"padding-bottom": "12px",
}}
>
<Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
<div style={{ display: "flex", "flex-direction": "column", gap: "6px", width: "100%" }}>
<Tabs.Trigger value="general">
<Icon name="sliders" />
{language.t("settings.tab.general")}
</Tabs.Trigger>
<Tabs.Trigger value="shortcuts">
<Icon name="keyboard" />
{language.t("settings.tab.shortcuts")}
</Tabs.Trigger>
<div class="flex flex-col justify-between h-full w-full">
<div class="flex flex-col gap-3 w-full pt-3">
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-1.5">
<Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
<div class="flex flex-col gap-1.5 w-full">
<Tabs.Trigger value="general">
<Icon name="sliders" />
{language.t("settings.tab.general")}
</Tabs.Trigger>
<Tabs.Trigger value="shortcuts">
<Icon name="keyboard" />
{language.t("settings.tab.shortcuts")}
</Tabs.Trigger>
</div>
</div>
<div class="flex flex-col gap-1.5">
<Tabs.SectionTitle>{language.t("settings.section.server")}</Tabs.SectionTitle>
<div class="flex flex-col gap-1.5 w-full">
<Tabs.Trigger value="providers">
<Icon name="server" />
{language.t("settings.providers.title")}
</Tabs.Trigger>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
<span>OpenCode Desktop</span>
<span class="text-11-regular">v{platform.version}</span>
</div>
</div>
{/* <Tabs.SectionTitle>Server</Tabs.SectionTitle> */}
{/* <Tabs.Trigger value="permissions"> */}
{/* <Icon name="checklist" /> */}
{/* Permissions */}
{/* </Tabs.Trigger> */}
{/* <Tabs.Trigger value="providers"> */}
{/* <Icon name="server" /> */}
{/* Providers */}
{/* </Tabs.Trigger> */}
{/* <Tabs.Trigger value="models"> */}
{/* <Icon name="brain" /> */}
{/* Models */}
{/* </Tabs.Trigger> */}
{/* <Tabs.Trigger value="agents"> */}
{/* <Icon name="task" /> */}
{/* Agents */}
{/* </Tabs.Trigger> */}
{/* <Tabs.Trigger value="commands"> */}
{/* <Icon name="console" /> */}
{/* Commands */}
{/* </Tabs.Trigger> */}
{/* <Tabs.Trigger value="mcp"> */}
{/* <Icon name="mcp" /> */}
{/* MCP */}
{/* </Tabs.Trigger> */}
</Tabs.List>
<Tabs.Content value="general" class="no-scrollbar">
<SettingsGeneral />
@@ -73,12 +61,9 @@ export const DialogSettings: Component = () => {
<Tabs.Content value="shortcuts" class="no-scrollbar">
<SettingsKeybinds />
</Tabs.Content>
{/* <Tabs.Content value="permissions" class="no-scrollbar"> */}
{/* <SettingsPermissions /> */}
{/* </Tabs.Content> */}
{/* <Tabs.Content value="providers" class="no-scrollbar"> */}
{/* <SettingsProviders /> */}
{/* </Tabs.Content> */}
<Tabs.Content value="providers" class="no-scrollbar">
<SettingsProviders />
</Tabs.Content>
{/* <Tabs.Content value="models" class="no-scrollbar"> */}
{/* <SettingsModels /> */}
{/* </Tabs.Content> */}

View File

@@ -30,6 +30,7 @@ import { useLayout } from "@/context/layout"
import { useSDK } from "@/context/sdk"
import { useNavigate, useParams } from "@solidjs/router"
import { useSync } from "@/context/sync"
import { useComments } from "@/context/comments"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
@@ -38,7 +39,7 @@ import type { IconName } from "@opencode-ai/ui/icons/provider"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Select } from "@opencode-ai/ui/select"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/util/path"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { ModelSelectorPopover } from "@/components/dialog-select-model"
@@ -47,6 +48,7 @@ import { useProviders } from "@/hooks/use-providers"
import { useCommand } from "@/context/command"
import { Persist, persisted } from "@/utils/persist"
import { Identifier } from "@/utils/id"
import { Worktree as WorktreeState } from "@/utils/worktree"
import { SessionContextUsage } from "@/components/session-context-usage"
import { usePermission } from "@/context/permission"
import { useLanguage } from "@/context/language"
@@ -60,11 +62,19 @@ import { base64Encode } from "@opencode-ai/util/encode"
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
type PendingPrompt = {
abort: AbortController
cleanup: VoidFunction
}
const pending = new Map<string, PendingPrompt>()
interface PromptInputProps {
class?: string
ref?: (el: HTMLDivElement) => void
newSessionWorktree?: string
onNewSessionWorktreeReset?: () => void
onSubmit?: () => void
}
const EXAMPLES = [
@@ -113,7 +123,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const local = useLocal()
const files = useFile()
const prompt = usePrompt()
const commentCount = createMemo(() => prompt.context.items().filter((item) => !!item.comment?.trim()).length)
const layout = useLayout()
const comments = useComments()
const params = useParams()
const dialog = useDialog()
const providers = useProviders()
@@ -125,6 +137,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
let scrollRef!: HTMLDivElement
let slashPopoverRef!: HTMLDivElement
const mirror = { input: false }
const scrollCursorIntoView = () => {
const container = scrollRef
const selection = window.getSelection()
@@ -156,11 +170,59 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey()))
const activeFile = createMemo(() => {
const tab = tabs().active()
if (!tab) return
return files.pathFromTab(tab)
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
const commentInReview = (path: string) => {
const sessionID = params.id
if (!sessionID) return false
const diffs = sync.data.session_diff[sessionID]
if (!diffs) return false
return diffs.some((diff) => diff.file === path)
}
const openComment = (item: { path: string; commentID?: string; commentOrigin?: "review" | "file" }) => {
if (!item.commentID) return
const focus = { file: item.path, id: item.commentID }
comments.setActive(focus)
view().reviewPanel.open()
if (item.commentOrigin === "review") {
tabs().open("review")
requestAnimationFrame(() => comments.setFocus(focus))
return
}
if (item.commentOrigin !== "file" && commentInReview(item.path)) {
tabs().open("review")
requestAnimationFrame(() => comments.setFocus(focus))
return
}
const tab = files.tab(item.path)
tabs().open(tab)
files.load(item.path)
requestAnimationFrame(() => comments.setFocus(focus))
}
const recent = createMemo(() => {
const all = tabs().all()
const active = tabs().active()
const order = active ? [active, ...all.filter((x) => x !== active)] : all
const seen = new Set<string>()
const paths: string[] = []
for (const tab of order) {
const path = files.pathFromTab(tab)
if (!path) continue
if (seen.has(path)) continue
seen.add(path)
paths.push(path)
}
return paths
})
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const status = createMemo(
@@ -381,7 +443,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (!isFocused()) setComposing(false)
})
type AtOption = { type: "agent"; name: string; display: string } | { type: "file"; path: string; display: string }
type AtOption =
| { type: "agent"; name: string; display: string }
| { type: "file"; path: string; display: string; recent?: boolean }
const agentList = createMemo(() =>
sync.data.agent
@@ -412,12 +476,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
} = useFilteredList<AtOption>({
items: async (query) => {
const agents = agentList()
const open = recent()
const seen = new Set(open)
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
const paths = await files.searchFilesAndDirectories(query)
const fileOptions: AtOption[] = paths.map((path) => ({ type: "file", path, display: path }))
return [...agents, ...fileOptions]
const fileOptions: AtOption[] = paths
.filter((path) => !seen.has(path))
.map((path) => ({ type: "file", path, display: path }))
return [...agents, ...pinned, ...fileOptions]
},
key: atKey,
filterKeys: ["display"],
groupBy: (item) => {
if (item.type === "agent") return "agent"
if (item.recent) return "recent"
return "file"
},
sortGroupsBy: (a, b) => {
const rank = (category: string) => {
if (category === "agent") return 0
if (category === "recent") return 1
return 2
}
return rank(a.category) - rank(b.category)
},
onSelect: handleAtSelect,
})
@@ -574,6 +656,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
() => prompt.current(),
(currentParts) => {
const inputParts = currentParts.filter((part) => part.type !== "image") as Prompt
if (mirror.input) {
mirror.input = false
if (isNormalizedEditor()) return
const selection = window.getSelection()
let cursorPosition: number | null = null
if (selection && selection.rangeCount > 0 && editorRef.contains(selection.anchorNode)) {
cursorPosition = getCursorPosition(editorRef)
}
renderEditor(inputParts)
if (cursorPosition !== null) {
setCursorPosition(editorRef, cursorPosition)
}
return
}
const domParts = parseFromDOM()
if (isNormalizedEditor() && isPromptEqual(inputParts, domParts)) return
@@ -688,6 +789,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setStore("savedPrompt", null)
}
if (prompt.dirty()) {
mirror.input = true
prompt.set(DEFAULT_PROMPT, 0)
}
queueScroll()
@@ -718,6 +820,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setStore("savedPrompt", null)
}
mirror.input = true
prompt.set([...rawParts, ...images], cursorPosition)
queueScroll()
}
@@ -809,12 +912,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setStore("popover", null)
}
const abort = () =>
sdk.client.session
const abort = async () => {
const sessionID = params.id
if (!sessionID) return Promise.resolve()
const queued = pending.get(sessionID)
if (queued) {
queued.abort.abort()
queued.cleanup()
pending.delete(sessionID)
return Promise.resolve()
}
return sdk.client.session
.abort({
sessionID: params.id!,
sessionID,
})
.catch(() => {})
}
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
const text = prompt
@@ -1074,6 +1187,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
return
}
WorktreeState.pending(createdWorktree.directory)
sessionDirectory = createdWorktree.directory
}
@@ -1110,6 +1224,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
if (!session) return
props.onSubmit?.()
const model = {
modelID: currentModel.id,
providerID: currentModel.provider.id,
@@ -1228,37 +1344,69 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const usedUrls = new Set(fileAttachmentParts.map((part) => part.url))
const contextFileParts: Array<{
id: string
type: "file"
mime: string
url: string
filename?: string
}> = []
const context = prompt.context.items().slice()
const addContextFile = (path: string, selection?: FileSelection) => {
const absolute = toAbsolutePath(path)
const query = selection ? `?start=${selection.startLine}&end=${selection.endLine}` : ""
const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim())
const contextParts: Array<
| {
id: string
type: "text"
text: string
synthetic?: boolean
}
| {
id: string
type: "file"
mime: string
url: string
filename?: string
}
> = []
const commentNote = (path: string, selection: FileSelection | undefined, comment: string) => {
const start = selection ? Math.min(selection.startLine, selection.endLine) : undefined
const end = selection ? Math.max(selection.startLine, selection.endLine) : undefined
const range =
start === undefined || end === undefined
? "this file"
: start === end
? `line ${start}`
: `lines ${start} through ${end}`
return `The user made the following comment regarding ${range} of ${path}: ${comment}`
}
const addContextFile = (input: { path: string; selection?: FileSelection; comment?: string }) => {
const absolute = toAbsolutePath(input.path)
const query = input.selection ? `?start=${input.selection.startLine}&end=${input.selection.endLine}` : ""
const url = `file://${absolute}${query}`
if (usedUrls.has(url)) return
const comment = input.comment?.trim()
if (!comment && usedUrls.has(url)) return
usedUrls.add(url)
contextFileParts.push({
if (comment) {
contextParts.push({
id: Identifier.ascending("part"),
type: "text",
text: commentNote(input.path, input.selection, comment),
synthetic: true,
})
}
contextParts.push({
id: Identifier.ascending("part"),
type: "file",
mime: "text/plain",
url,
filename: getFilename(path),
filename: getFilename(input.path),
})
}
const activePath = activeFile()
if (activePath && prompt.context.activeTab()) {
addContextFile(activePath)
}
for (const item of prompt.context.items()) {
for (const item of context) {
if (item.type !== "file") continue
addContextFile(item.path, item.selection)
addContextFile({ path: item.path, selection: item.selection, comment: item.comment })
}
const imageAttachmentParts = images.map((attachment) => ({
@@ -1278,7 +1426,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const requestParts = [
textPart,
...fileAttachmentParts,
...contextFileParts,
...contextParts,
...agentAttachmentParts,
...imageAttachmentParts,
]
@@ -1298,10 +1446,27 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
model,
}
const setSyncStore = sessionDirectory === projectDirectory ? sync.set : globalSync.child(sessionDirectory)[1]
const addOptimisticMessage = () => {
setSyncStore(
if (sessionDirectory === projectDirectory) {
sync.set(
produce((draft) => {
const messages = draft.message[session.id]
if (!messages) {
draft.message[session.id] = [optimisticMessage]
} else {
const result = Binary.search(messages, messageID, (m) => m.id)
messages.splice(result.index, 0, optimisticMessage)
}
draft.part[messageID] = optimisticParts
.filter((p) => !!p?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
}),
)
return
}
globalSync.child(sessionDirectory)[1](
produce((draft) => {
const messages = draft.message[session.id]
if (!messages) {
@@ -1319,7 +1484,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
const removeOptimisticMessage = () => {
setSyncStore(
if (sessionDirectory === projectDirectory) {
sync.set(
produce((draft) => {
const messages = draft.message[session.id]
if (messages) {
const result = Binary.search(messages, messageID, (m) => m.id)
if (result.found) messages.splice(result.index, 1)
}
delete draft.part[messageID]
}),
)
return
}
globalSync.child(sessionDirectory)[1](
produce((draft) => {
const messages = draft.message[session.id]
if (messages) {
@@ -1331,11 +1510,76 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
)
}
for (const item of commentItems) {
prompt.context.remove(item.key)
}
clearInput()
addOptimisticMessage()
client.session
.prompt({
const waitForWorktree = async () => {
const worktree = WorktreeState.get(sessionDirectory)
if (!worktree || worktree.status !== "pending") return true
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "busy" })
}
const controller = new AbortController()
const cleanup = () => {
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "idle" })
}
removeOptimisticMessage()
for (const item of commentItems) {
prompt.context.add({
type: "file",
path: item.path,
selection: item.selection,
comment: item.comment,
commentID: item.commentID,
commentOrigin: item.commentOrigin,
preview: item.preview,
})
}
restoreInput()
}
pending.set(session.id, { abort: controller, cleanup })
const abort = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
if (controller.signal.aborted) {
resolve({ status: "failed", message: "aborted" })
return
}
controller.signal.addEventListener(
"abort",
() => {
resolve({ status: "failed", message: "aborted" })
},
{ once: true },
)
})
const timeoutMs = 5 * 60 * 1000
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
setTimeout(() => {
resolve({ status: "failed", message: "Workspace is still preparing" })
}, timeoutMs)
})
const result = await Promise.race([WorktreeState.wait(sessionDirectory), abort, timeout])
pending.delete(session.id)
if (controller.signal.aborted) return false
if (result.status === "failed") throw new Error(result.message)
return true
}
const send = async () => {
const ok = await waitForWorktree()
if (!ok) return
await client.session.prompt({
sessionID: session.id,
agent,
model,
@@ -1343,14 +1587,31 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
parts: requestParts,
variant,
})
.catch((err) => {
showToast({
title: language.t("prompt.toast.promptSendFailed.title"),
description: errorMessage(err),
})
removeOptimisticMessage()
restoreInput()
}
void send().catch((err) => {
pending.delete(session.id)
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "idle" })
}
showToast({
title: language.t("prompt.toast.promptSendFailed.title"),
description: errorMessage(err),
})
removeOptimisticMessage()
for (const item of commentItems) {
prompt.context.add({
type: "file",
path: item.path,
selection: item.selection,
comment: item.comment,
commentID: item.commentID,
commentOrigin: item.commentOrigin,
preview: item.preview,
})
}
restoreInput()
})
}
return (
@@ -1391,7 +1652,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/>
<div class="flex items-center text-14-regular min-w-0">
<span class="text-text-weak whitespace-nowrap truncate min-w-0">
{getDirectory((item as { type: "file"; path: string }).path)}
{(() => {
const path = (item as { type: "file"; path: string }).path
return path.endsWith("/") ? path : getDirectory(path)
})()}
</span>
<Show when={!(item as { type: "file"; path: string }).path.endsWith("/")}>
<span class="text-text-strong whitespace-nowrap">
@@ -1457,7 +1721,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
classList={{
"group/prompt-input": true,
"bg-surface-raised-stronger-non-alpha shadow-xs-border relative": true,
"rounded-md overflow-clip focus-within:shadow-xs-border": true,
"rounded-[14px] overflow-clip focus-within:shadow-xs-border": true,
"border-icon-info-active border-dashed": store.dragging,
[props.class ?? ""]: !!props.class,
}}
@@ -1470,63 +1734,81 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</div>
</div>
</Show>
<Show when={false && (prompt.context.items().length > 0 || !!activeFile())}>
<div class="flex flex-wrap items-center gap-2 px-3 pt-3">
<Show when={prompt.context.activeTab() ? activeFile() : undefined}>
{(path) => (
<div class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base max-w-full">
<FileIcon node={{ path: path(), type: "file" }} class="shrink-0 size-4" />
<div class="flex items-center text-12-regular min-w-0">
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(path())}</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(path())}</span>
<span class="text-text-weak whitespace-nowrap ml-1">{language.t("prompt.context.active")}</span>
</div>
<IconButton
type="button"
icon="close"
variant="ghost"
class="h-6 w-6"
onClick={() => prompt.context.removeActive()}
/>
</div>
)}
</Show>
<Show when={!prompt.context.activeTab() && !!activeFile()}>
<button
type="button"
class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base text-12-regular text-text-weak hover:bg-surface-raised-base-hover"
onClick={() => prompt.context.addActive()}
>
<Icon name="plus-small" size="small" />
<span>{language.t("prompt.context.includeActiveFile")}</span>
</button>
</Show>
<Show when={prompt.context.items().length > 0}>
<div class="flex flex-nowrap items-start gap-2 p-2 overflow-x-auto no-scrollbar">
<For each={prompt.context.items()}>
{(item) => (
<div class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base max-w-full">
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-4" />
<div class="flex items-center text-12-regular min-w-0">
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(item.path)}</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path)}</span>
<Show when={item.selection}>
{(sel) => (
<span class="text-text-weak whitespace-nowrap ml-1">
{sel().startLine === sel().endLine
? `:${sel().startLine}`
: `:${sel().startLine}-${sel().endLine}`}
{(item) => {
const active = () => {
const a = comments.active()
return !!item.commentID && item.commentID === a?.id && item.path === a?.file
}
return (
<Tooltip
value={
<span class="flex max-w-[300px]">
<span
class="text-text-invert-base truncate min-w-0"
style={{ direction: "rtl", "text-align": "left", "unicode-bidi": "plaintext" }}
>
{getDirectory(item.path)}
</span>
)}
</Show>
</div>
<IconButton
type="button"
icon="close"
variant="ghost"
class="h-6 w-6"
onClick={() => prompt.context.remove(item.key)}
/>
</div>
)}
<span class="shrink-0">{getFilename(item.path)}</span>
</span>
}
placement="top"
openDelay={2000}
>
<div
classList={{
"group shrink-0 flex flex-col rounded-[6px] pl-2 pr-1 py-1 max-w-[200px] h-12 transition-all transition-transform shadow-xs-border hover:shadow-xs-border-hover": true,
"cursor-pointer hover:bg-surface-interactive-weak": !!item.commentID && !active(),
"cursor-pointer bg-surface-interactive-hover hover:bg-surface-interactive-hover shadow-xs-border-hover":
active(),
"bg-background-stronger": !active(),
}}
onClick={() => {
openComment(item)
}}
>
<div class="flex items-center gap-1.5">
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
<div
class="flex items-center text-11-regular min-w-0"
style={{ "font-weight": "var(--font-weight-medium)" }}
>
<span class="text-text-strong whitespace-nowrap">{getFilenameTruncated(item.path, 14)}</span>
<Show when={item.selection}>
{(sel) => (
<span class="text-text-weak whitespace-nowrap shrink-0">
{sel().startLine === sel().endLine
? `:${sel().startLine}`
: `:${sel().startLine}-${sel().endLine}`}
</span>
)}
</Show>
</div>
<IconButton
type="button"
icon="close-small"
variant="ghost"
class="ml-auto h-5 w-5 opacity-0 group-hover:opacity-100 transition-all"
onClick={(e) => {
e.stopPropagation()
if (item.commentID) comments.remove(item.path, item.commentID)
prompt.context.remove(item.key)
}}
aria-label={language.t("prompt.context.removeFile")}
/>
</div>
<Show when={item.comment}>
{(comment) => (
<div class="text-12-regular text-text-strong ml-5 pr-1 truncate">{comment()}</div>
)}
</Show>
</div>
</Tooltip>
)
}}
</For>
</div>
</Show>
@@ -1556,6 +1838,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
type="button"
onClick={() => removeImageAttachment(attachment.id)}
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover"
aria-label={language.t("prompt.attachment.remove")}
>
<Icon name="close" class="size-3 text-text-weak" />
</button>
@@ -1574,6 +1857,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
editorRef = el
props.ref?.(el)
}}
role="textbox"
aria-multiline="true"
aria-label={
store.mode === "shell"
? language.t("prompt.placeholder.shell")
: commentCount() > 1
? "Summarize comments…"
: commentCount() === 1
? "Summarize comment…"
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })
}
contenteditable="true"
onInput={handleInput}
onPaste={handlePaste}
@@ -1582,17 +1876,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onKeyDown={handleKeyDown}
classList={{
"select-text": true,
"w-full px-5 py-3 pr-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
"w-full p-3 pr-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
"[&_[data-type=file]]:text-syntax-property": true,
"[&_[data-type=agent]]:text-syntax-type": true,
"font-mono!": store.mode === "shell",
}}
/>
<Show when={!prompt.dirty()}>
<div class="absolute top-0 inset-x-0 px-5 py-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
<div class="absolute top-0 inset-x-0 p-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
{store.mode === "shell"
? language.t("prompt.placeholder.shell")
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })}
: commentCount() > 1
? "Summarize comments…"
: commentCount() === 1
? "Summarize comment…"
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })}
</div>
</Show>
</div>
@@ -1638,21 +1936,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</TooltipKeybind>
}
>
<ModelSelectorPopover>
<TooltipKeybind
placement="top"
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
<Button as="div" variant="ghost">
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
</Show>
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
<Icon name="chevron-down" size="small" />
</Button>
</TooltipKeybind>
</ModelSelectorPopover>
<TooltipKeybind
placement="top"
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
<ModelSelectorPopover triggerAs={Button} triggerProps={{ variant: "ghost" }}>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
</Show>
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
<Icon name="chevron-down" size="small" />
</ModelSelectorPopover>
</TooltipKeybind>
</Show>
<Show when={local.model.variant.list().length > 0}>
<TooltipKeybind
@@ -1683,6 +1979,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
}}
aria-label={
permission.isAutoAccepting(params.id!, sdk.directory)
? language.t("command.permissions.autoaccept.disable")
: language.t("command.permissions.autoaccept.enable")
}
aria-pressed={permission.isAutoAccepting(params.id!, sdk.directory)}
>
<Icon
name="chevron-double-right"
@@ -1695,7 +1997,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Match>
</Switch>
</div>
<div class="flex items-center gap-3 absolute right-2 bottom-2">
<div class="flex items-center gap-3 absolute right-3 bottom-3">
<input
ref={fileInputRef}
type="file"
@@ -1711,7 +2013,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<SessionContextUsage />
<Show when={store.mode === "normal"}>
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
<Button type="button" variant="ghost" class="size-6" onClick={() => fileInputRef.click()}>
<Button
type="button"
variant="ghost"
class="size-6"
onClick={() => fileInputRef.click()}
aria-label={language.t("prompt.action.attachFile")}
>
<Icon name="photo" class="size-4.5" />
</Button>
</Tooltip>
@@ -1743,6 +2051,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
icon={working() ? "stop" : "arrow-up"}
variant="primary"
class="h-6 w-4.5"
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
/>
</Tooltip>
</div>

View File

@@ -0,0 +1,31 @@
import { onMount } from "solid-js"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogReleaseNotes } from "./dialog-release-notes"
import { shouldShowReleaseNotes, markReleaseNotesSeen } from "@/lib/release-notes"
/**
* Component that handles showing release notes modal on app startup.
* Shows the modal if:
* - DEV_ALWAYS_SHOW_RELEASE_NOTES is true in lib/release-notes.ts
* - OR the user hasn't seen the current version's release notes yet
*
* To disable the dev mode behavior, set DEV_ALWAYS_SHOW_RELEASE_NOTES to false
* in packages/app/src/lib/release-notes.ts
*/
export function ReleaseNotesHandler() {
const dialog = useDialog()
onMount(() => {
// Small delay to ensure app is fully loaded before showing modal
setTimeout(() => {
if (shouldShowReleaseNotes()) {
dialog.show(
() => <DialogReleaseNotes />,
() => markReleaseNotesSeen(),
)
}
}, 500)
})
return null
}

View File

@@ -4,6 +4,7 @@ import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import { Button } from "@opencode-ai/ui/button"
import { useParams } from "@solidjs/router"
import { AssistantMessage } from "@opencode-ai/sdk/v2/client"
import { findLast } from "@opencode-ai/util/array"
import { useLayout } from "@/context/layout"
import { useSync } from "@/context/sync"
@@ -21,22 +22,26 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const variant = createMemo(() => props.variant ?? "button")
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey()))
const view = createMemo(() => layout.view(sessionKey()))
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
const usd = createMemo(
() =>
new Intl.NumberFormat(language.locale(), {
style: "currency",
currency: "USD",
}),
)
const cost = createMemo(() => {
const locale = language.locale()
const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
return new Intl.NumberFormat(locale, {
style: "currency",
currency: "USD",
}).format(total)
return usd().format(total)
})
const context = createMemo(() => {
const locale = language.locale()
const last = messages().findLast((x) => {
const last = findLast(messages(), (x) => {
if (x.role !== "assistant") return false
const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
return total > 0
@@ -84,9 +89,6 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
<span class="text-text-invert-strong">{cost()}</span>
<span class="text-text-invert-base">{language.t("context.usage.cost")}</span>
</div>
<Show when={variant() === "button"}>
<div class="text-11-regular text-text-invert-base mt-1">{language.t("context.usage.clickToView")}</div>
</Show>
</div>
)
@@ -96,7 +98,13 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
<Switch>
<Match when={variant() === "indicator"}>{circle()}</Match>
<Match when={true}>
<Button type="button" variant="ghost" class="size-6" onClick={openContext}>
<Button
type="button"
variant="ghost"
class="size-6"
onClick={openContext}
aria-label={language.t("context.usage.view")}
>
{circle()}
</Button>
</Match>

View File

@@ -1,42 +0,0 @@
import { createMemo, Show } from "solid-js"
import { useSync } from "@/context/sync"
import { useLanguage } from "@/context/language"
import { Tooltip } from "@opencode-ai/ui/tooltip"
export function SessionLspIndicator() {
const sync = useSync()
const language = useLanguage()
const lspStats = createMemo(() => {
const lsp = sync.data.lsp ?? []
const connected = lsp.filter((s) => s.status === "connected").length
const hasError = lsp.some((s) => s.status === "error")
const total = lsp.length
return { connected, hasError, total }
})
const tooltipContent = createMemo(() => {
const lsp = sync.data.lsp ?? []
if (lsp.length === 0) return language.t("lsp.tooltip.none")
return lsp.map((s) => s.name).join(", ")
})
return (
<Show when={lspStats().total > 0}>
<Tooltip placement="top" value={tooltipContent()}>
<div class="flex items-center gap-1 px-2 cursor-default select-none">
<div
classList={{
"size-1.5 rounded-full": true,
"bg-icon-critical-base": lspStats().hasError,
"bg-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
}}
/>
<span class="text-12-regular text-text-weak">
{language.t("lsp.label.connected", { count: lspStats().connected })}
</span>
</div>
</Tooltip>
</Show>
)
}

View File

@@ -1,34 +0,0 @@
import { createMemo, Show } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useSync } from "@/context/sync"
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
export function SessionMcpIndicator() {
const sync = useSync()
const dialog = useDialog()
const mcpStats = createMemo(() => {
const mcp = sync.data.mcp ?? {}
const entries = Object.entries(mcp)
const enabled = entries.filter(([, status]) => status.status === "connected").length
const failed = entries.some(([, status]) => status.status === "failed")
const total = entries.length
return { enabled, failed, total }
})
return (
<Show when={mcpStats().total > 0}>
<Button variant="ghost" onClick={() => dialog.show(() => <DialogSelectMcp />)}>
<div
classList={{
"size-1.5 rounded-full": true,
"bg-icon-critical-base": mcpStats().failed,
"bg-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0,
}}
/>
<span class="text-12-regular text-text-weak">{mcpStats().enabled} MCP</span>
</Button>
</Show>
)
}

View File

@@ -5,6 +5,7 @@ import { DateTime } from "luxon"
import { useSync } from "@/context/sync"
import { useLayout } from "@/context/layout"
import { checksum } from "@opencode-ai/util/encode"
import { findLast } from "@opencode-ai/util/array"
import { Icon } from "@opencode-ai/ui/icon"
import { Accordion } from "@opencode-ai/ui/accordion"
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
@@ -25,8 +26,16 @@ export function SessionContextTab(props: SessionContextTabProps) {
const sync = useSync()
const language = useLanguage()
const usd = createMemo(
() =>
new Intl.NumberFormat(language.locale(), {
style: "currency",
currency: "USD",
}),
)
const ctx = createMemo(() => {
const last = props.messages().findLast((x) => {
const last = findLast(props.messages(), (x) => {
if (x.role !== "assistant") return false
const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
return total > 0
@@ -61,12 +70,8 @@ export function SessionContextTab(props: SessionContextTabProps) {
})
const cost = createMemo(() => {
const locale = language.locale()
const total = props.messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
return new Intl.NumberFormat(locale, {
style: "currency",
currency: "USD",
}).format(total)
return usd().format(total)
})
const counts = createMemo(() => {
@@ -81,7 +86,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
})
const systemPrompt = createMemo(() => {
const msg = props.visibleUserMessages().findLast((m) => !!m.system)
const msg = findLast(props.visibleUserMessages(), (m) => !!m.system)
const system = msg?.system
if (!system) return
const trimmed = system.trim()
@@ -282,7 +287,9 @@ export function SessionContextTab(props: SessionContextTabProps) {
}
})
return <Code file={file()} overflow="wrap" class="select-text" />
return (
<Code file={file()} overflow="wrap" class="select-text" onRendered={() => requestAnimationFrame(restoreScroll)} />
)
}
function RawMessage(msgProps: { message: Message }) {
@@ -314,19 +321,13 @@ export function SessionContextTab(props: SessionContextTabProps) {
let frame: number | undefined
let pending: { x: number; y: number } | undefined
const restoreScroll = (retries = 0) => {
const restoreScroll = () => {
const el = scroll
if (!el) return
const s = props.view()?.scroll("context")
if (!s) return
// Wait for content to be scrollable - content may not have rendered yet
if (el.scrollHeight <= el.clientHeight && retries < 10) {
requestAnimationFrame(() => restoreScroll(retries + 1))
return
}
if (el.scrollTop !== s.y) el.scrollTop = s.y
if (el.scrollLeft !== s.x) el.scrollLeft = s.x
}

View File

@@ -5,8 +5,6 @@ import { useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
// import { useServer } from "@/context/server"
// import { useDialog } from "@opencode-ai/ui/context/dialog"
import { usePlatform } from "@/context/platform"
import { useSync } from "@/context/sync"
import { useGlobalSDK } from "@/context/global-sdk"
@@ -20,14 +18,13 @@ import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { Popover } from "@opencode-ai/ui/popover"
import { TextField } from "@opencode-ai/ui/text-field"
import { Keybind } from "@opencode-ai/ui/keybind"
import { StatusPopover } from "../status-popover"
export function SessionHeader() {
const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
const command = useCommand()
// const server = useServer()
// const dialog = useDialog()
const sync = useSync()
const platform = usePlatform()
const language = useLanguage()
@@ -47,10 +44,10 @@ export function SessionHeader() {
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
const showReview = createMemo(() => !!currentSession()?.summary?.files)
const showShare = createMemo(() => shareEnabled() && !!currentSession())
const showReview = createMemo(() => !!currentSession())
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const view = createMemo(() => layout.view(sessionKey()))
const view = createMemo(() => layout.view(sessionKey))
const [state, setState] = createStore({
share: false,
@@ -136,6 +133,7 @@ export function SessionHeader() {
type="button"
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
onClick={() => command.trigger("file.open")}
aria-label={language.t("session.header.searchFiles")}
>
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-visible">
<Icon name="magnifying-glass" size="normal" class="icon-base shrink-0" />
@@ -153,74 +151,114 @@ export function SessionHeader() {
{(mount) => (
<Portal mount={mount()}>
<div class="flex items-center gap-3">
{/* <div class="hidden md:flex items-center gap-1"> */}
{/* <Button */}
{/* size="small" */}
{/* variant="ghost" */}
{/* onClick={() => { */}
{/* dialog.show(() => <DialogSelectServer />) */}
{/* }} */}
{/* > */}
{/* <div */}
{/* classList={{ */}
{/* "size-1.5 rounded-full": true, */}
{/* "bg-icon-success-base": server.healthy() === true, */}
{/* "bg-icon-critical-base": server.healthy() === false, */}
{/* "bg-border-weak-base": server.healthy() === undefined, */}
{/* }} */}
{/* /> */}
{/* <Icon name="server" size="small" class="text-icon-weak" /> */}
{/* <span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span> */}
{/* </Button> */}
{/* <SessionLspIndicator /> */}
{/* <SessionMcpIndicator /> */}
{/* </div> */}
<div class="flex items-center gap-1">
<div
class="hidden md:block shrink-0"
classList={{
"opacity-0 pointer-events-none": !showReview(),
}}
aria-hidden={!showReview()}
>
<TooltipKeybind
title={language.t("command.review.toggle")}
keybind={command.keybind("review.toggle")}
<StatusPopover />
<Show when={showShare()}>
<div class="flex items-center">
<Popover
title={language.t("session.share.popover.title")}
description={
shareUrl()
? language.t("session.share.popover.description.shared")
: language.t("session.share.popover.description.unshared")
}
gutter={6}
placement="bottom-end"
shift={-64}
class="rounded-xl [&_[data-slot=popover-close-button]]:hidden"
triggerAs={Button}
triggerProps={{
variant: "secondary",
class: "rounded-sm w-[60px] h-[24px]",
classList: { "rounded-r-none": shareUrl() !== undefined },
style: { scale: 1 },
}}
trigger={language.t("session.share.action.share")}
>
<Button
variant="ghost"
class="group/review-toggle size-6 p-0"
onClick={() => view().reviewPanel.toggle()}
<div class="flex flex-col gap-2">
<Show
when={shareUrl()}
fallback={
<div class="flex">
<Button
size="large"
variant="primary"
class="w-1/2"
onClick={shareSession}
disabled={state.share}
>
{state.share
? language.t("session.share.action.publishing")
: language.t("session.share.action.publish")}
</Button>
</div>
}
>
<div class="flex flex-col gap-2">
<TextField value={shareUrl() ?? ""} readOnly copyable tabIndex={-1} class="w-full" />
<div class="grid grid-cols-2 gap-2">
<Button
size="large"
variant="secondary"
class="w-full shadow-none border border-border-weak-base"
onClick={unshareSession}
disabled={state.unshare}
>
{state.unshare
? language.t("session.share.action.unpublishing")
: language.t("session.share.action.unpublish")}
</Button>
<Button
size="large"
variant="primary"
class="w-full"
onClick={viewShare}
disabled={state.unshare}
>
{language.t("session.share.action.view")}
</Button>
</div>
</div>
</Show>
</div>
</Popover>
<Show when={shareUrl()} fallback={<div aria-hidden="true" />}>
<Tooltip
value={
state.copied
? language.t("session.share.copy.copied")
: language.t("session.share.copy.copyLink")
}
placement="top"
gutter={8}
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-right"}
class="group-hover/review-toggle:hidden"
/>
<Icon
size="small"
name="layout-right-partial"
class="hidden group-hover/review-toggle:inline-block"
/>
<Icon
size="small"
name={view().reviewPanel.opened() ? "layout-right" : "layout-right-full"}
class="hidden group-active/review-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
<IconButton
icon={state.copied ? "check" : "link"}
variant="secondary"
class="rounded-l-none"
onClick={copyLink}
disabled={state.unshare}
aria-label={
state.copied
? language.t("session.share.copy.copied")
: language.t("session.share.copy.copyLink")
}
/>
</Tooltip>
</Show>
</div>
</Show>
<div class="hidden md:flex items-center gap-3 ml-2 shrink-0">
<TooltipKeybind
class="hidden md:block shrink-0"
title={language.t("command.terminal.toggle")}
keybind={command.keybind("terminal.toggle")}
>
<Button
variant="ghost"
class="group/terminal-toggle size-8 rounded-md"
class="group/terminal-toggle size-6 p-0"
onClick={() => view().terminal.toggle()}
aria-label={language.t("command.terminal.toggle")}
aria-expanded={view().terminal.opened()}
aria-controls="terminal-panel"
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
@@ -242,96 +280,36 @@ export function SessionHeader() {
</Button>
</TooltipKeybind>
</div>
<div
class="flex items-center"
classList={{
"opacity-0 pointer-events-none": !showShare(),
}}
aria-hidden={!showShare()}
>
<Popover
title={language.t("session.share.popover.title")}
description={
shareUrl()
? language.t("session.share.popover.description.shared")
: language.t("session.share.popover.description.unshared")
}
trigger={
<Tooltip class="shrink-0" value={language.t("command.session.share")}>
<Button
variant="secondary"
classList={{ "rounded-r-none": shareUrl() !== undefined }}
style={{ scale: 1 }}
>
{language.t("session.share.action.share")}
</Button>
</Tooltip>
}
>
<div class="flex flex-col gap-2">
<Show
when={shareUrl()}
fallback={
<div class="flex">
<Button
size="large"
variant="primary"
class="w-1/2"
onClick={shareSession}
disabled={state.share}
>
{state.share
? language.t("session.share.action.publishing")
: language.t("session.share.action.publish")}
</Button>
</div>
}
>
<div class="flex flex-col gap-2 w-72">
<TextField value={shareUrl() ?? ""} readOnly copyable class="w-full" />
<div class="grid grid-cols-2 gap-2">
<Button
size="large"
variant="secondary"
class="w-full shadow-none border border-border-weak-base"
onClick={unshareSession}
disabled={state.unshare}
>
{state.unshare
? language.t("session.share.action.unpublishing")
: language.t("session.share.action.unpublish")}
</Button>
<Button
size="large"
variant="primary"
class="w-full"
onClick={viewShare}
disabled={state.unshare}
>
{language.t("session.share.action.view")}
</Button>
</div>
</div>
</Show>
</div>
</Popover>
<Show when={shareUrl()} fallback={<div class="size-6" aria-hidden="true" />}>
<Tooltip
value={
state.copied ? language.t("session.share.copy.copied") : language.t("session.share.copy.copyLink")
}
placement="top"
gutter={8}
<div class="hidden md:block shrink-0">
<TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}>
<Button
variant="ghost"
class="group/review-toggle size-6 p-0"
onClick={() => view().reviewPanel.toggle()}
aria-label={language.t("command.review.toggle")}
aria-expanded={view().reviewPanel.opened()}
aria-controls="review-panel"
tabIndex={showReview() ? 0 : -1}
>
<IconButton
icon={state.copied ? "check" : "copy"}
variant="secondary"
class="rounded-l-none"
onClick={copyLink}
disabled={state.unshare}
/>
</Tooltip>
</Show>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-right"}
class="group-hover/review-toggle:hidden"
/>
<Icon
size="small"
name="layout-right-partial"
class="hidden group-hover/review-toggle:inline-block"
/>
<Icon
size="small"
name={view().reviewPanel.opened() ? "layout-right" : "layout-right-full"}
class="hidden group-active/review-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
</div>
</div>
</Portal>

View File

@@ -4,7 +4,6 @@ import { useSync } from "@/context/sync"
import { useLanguage } from "@/context/language"
import { Icon } from "@opencode-ai/ui/icon"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Select } from "@opencode-ai/ui/select"
const MAIN_WORKTREE = "main"
const CREATE_WORKTREE = "create"
@@ -60,18 +59,7 @@ export function NewSessionView(props: NewSessionViewProps) {
</div>
<div class="flex justify-center items-center gap-1">
<Icon name="branch" size="small" />
<Select
options={options()}
current={current()}
value={(x) => x}
label={label}
onSelect={(value) => {
props.onWorktreeChange(value ?? MAIN_WORKTREE)
}}
size="normal"
variant="ghost"
class="text-12-medium"
/>
<div class="text-12-medium text-text-weak select-text ml-2">{label(current())}</div>
</div>
<Show when={sync.project}>
{(project) => (

View File

@@ -37,7 +37,12 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
value={props.tab}
closeButton={
<Tooltip value={language.t("common.closeTab")} placement="bottom">
<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />
<IconButton
icon="close"
variant="ghost"
onClick={() => props.onTabClose(props.tab)}
aria-label={language.t("common.closeTab")}
/>
</Tooltip>
}
hideCloseButton

View File

@@ -139,6 +139,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
e.stopPropagation()
close()
}}
aria-label={language.t("terminal.close")}
/>
}
>

View File

@@ -5,6 +5,26 @@ import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { useLanguage } from "@/context/language"
import { useSettings, monoFontFamily } from "@/context/settings"
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "./link"
let demoSoundState = {
cleanup: undefined as (() => void) | undefined,
timeout: undefined as NodeJS.Timeout | undefined,
}
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
// delay the playback by 100ms during quick selection changes and pause existing sounds.
const playDemoSound = (src: string) => {
if (demoSoundState.cleanup) {
demoSoundState.cleanup()
}
clearTimeout(demoSoundState.timeout)
demoSoundState.timeout = setTimeout(() => {
demoSoundState.cleanup = playSound(src)
}, 100)
}
export const SettingsGeneral: Component = () => {
const theme = useTheme()
@@ -35,6 +55,7 @@ export const SettingsGeneral: Component = () => {
{ value: "hack", label: "font.option.hack" },
{ value: "inconsolata", label: "font.option.inconsolata" },
{ value: "intel-one-mono", label: "font.option.intelOneMono" },
{ value: "iosevka", label: "font.option.iosevka" },
{ value: "jetbrains-mono", label: "font.option.jetbrainsMono" },
{ value: "meslo-lgs", label: "font.option.mesloLgs" },
{ value: "roboto-mono", label: "font.option.robotoMono" },
@@ -107,9 +128,7 @@ export const SettingsGeneral: Component = () => {
description={
<>
{language.t("settings.general.row.theme.description")}{" "}
<a href="#" class="text-text-interactive-base">
{language.t("common.learnMore")}
</a>
<Link href="https://opencode.ai/docs/themes/">{language.t("common.learnMore")}</Link>
</>
}
>
@@ -211,12 +230,12 @@ export const SettingsGeneral: Component = () => {
label={(o) => language.t(o.label)}
onHighlight={(option) => {
if (!option) return
playSound(option.src)
playDemoSound(option.src)
}}
onSelect={(option) => {
if (!option) return
settings.sounds.setAgent(option.id)
playSound(option.src)
playDemoSound(option.src)
}}
variant="secondary"
size="small"
@@ -235,12 +254,12 @@ export const SettingsGeneral: Component = () => {
label={(o) => language.t(o.label)}
onHighlight={(option) => {
if (!option) return
playSound(option.src)
playDemoSound(option.src)
}}
onSelect={(option) => {
if (!option) return
settings.sounds.setPermissions(option.id)
playSound(option.src)
playDemoSound(option.src)
}}
variant="secondary"
size="small"
@@ -259,12 +278,12 @@ export const SettingsGeneral: Component = () => {
label={(o) => language.t(o.label)}
onHighlight={(option) => {
if (!option) return
playSound(option.src)
playDemoSound(option.src)
}}
onSelect={(option) => {
if (!option) return
settings.sounds.setErrors(option.id)
playSound(option.src)
playDemoSound(option.src)
}}
variant="secondary"
size="small"

View File

@@ -1,14 +1,154 @@
import { Component } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tag } from "@opencode-ai/ui/tag"
import { showToast } from "@opencode-ai/ui/toast"
import type { IconName } from "@opencode-ai/ui/icons/provider"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { createMemo, type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language"
import { useGlobalSDK } from "@/context/global-sdk"
import { DialogConnectProvider } from "./dialog-connect-provider"
import { DialogSelectProvider } from "./dialog-select-provider"
type ProviderSource = "env" | "api" | "config" | "custom"
type ProviderMeta = { source?: ProviderSource }
export const SettingsProviders: Component = () => {
const dialog = useDialog()
const language = useLanguage()
const globalSDK = useGlobalSDK()
const providers = useProviders()
const connected = createMemo(() => providers.connected())
const popular = createMemo(() => {
const connectedIDs = new Set(connected().map((p) => p.id))
const items = providers
.popular()
.filter((p) => !connectedIDs.has(p.id))
.slice()
items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
return items
})
const source = (item: unknown) => (item as ProviderMeta).source
const type = (item: unknown) => {
const current = source(item)
if (current === "env") return language.t("settings.providers.tag.environment")
if (current === "api") return language.t("provider.connect.method.apiKey")
if (current === "config") return language.t("settings.providers.tag.config")
if (current === "custom") return language.t("settings.providers.tag.custom")
return language.t("settings.providers.tag.other")
}
const canDisconnect = (item: unknown) => source(item) !== "env"
const disconnect = async (providerID: string, name: string) => {
await globalSDK.client.auth
.remove({ providerID })
.then(async () => {
await globalSDK.client.global.dispose()
showToast({
variant: "success",
icon: "circle-check",
title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
})
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
}
return (
<div class="flex flex-col h-full overflow-y-auto">
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
<p class="text-14-regular text-text-weak">{language.t("settings.providers.description")}</p>
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
</div>
</div>
<div class="flex flex-col gap-8 max-w-[720px]">
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<Show
when={connected().length > 0}
fallback={
<div class="py-4 text-14-regular text-text-weak">
{language.t("settings.providers.connected.empty")}
</div>
}
>
<For each={connected()}>
{(item) => (
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
<div class="flex items-center gap-3 min-w-0">
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-regular text-text-strong truncate">{item.name}</span>
<Tag>{type(item)}</Tag>
</div>
<Show when={canDisconnect(item)}>
<Button size="small" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
{language.t("common.disconnect")}
</Button>
</Show>
</div>
)}
</For>
</Show>
</div>
</div>
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<For each={popular()}>
{(item) => (
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
<div class="flex items-center gap-x-3 min-w-0">
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-regular text-text-strong">{item.name}</span>
<Show when={item.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
<Show when={item.id === "anthropic"}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
</Show>
<Show when={item.id === "openai"}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
</Show>
<Show when={item.id.startsWith("github-copilot")}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
</Show>
</div>
<Button
size="large"
variant="secondary"
icon="plus-small"
onClick={() => {
dialog.show(() => <DialogConnectProvider provider={item.id} />)
}}
>
{language.t("common.connect")}
</Button>
</div>
)}
</For>
</div>
<Button
variant="ghost"
class="px-0 py-0 text-14-medium text-text-strong text-left justify-start hover:bg-transparent active:bg-transparent"
onClick={() => {
dialog.show(() => <DialogSelectProvider />)
}}
>
{language.t("dialog.provider.viewAll")}
</Button>
</div>
</div>
</div>
)

View File

@@ -0,0 +1,422 @@
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { useNavigate } from "@solidjs/router"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Popover } from "@opencode-ai/ui/popover"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Button } from "@opencode-ai/ui/button"
import { Switch } from "@opencode-ai/ui/switch"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useSync } from "@/context/sync"
import { useSDK } from "@/context/sdk"
import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server"
import { usePlatform } from "@/context/platform"
import { useLanguage } from "@/context/language"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { DialogSelectServer } from "./dialog-select-server"
type ServerStatus = { healthy: boolean; version?: string }
async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>): Promise<ServerStatus> {
const sdk = createOpencodeClient({
baseUrl: url,
fetch: platform.fetch,
signal: AbortSignal.timeout(3000),
})
return sdk.global
.health()
.then((x) => ({ healthy: x.data?.healthy === true, version: x.data?.version }))
.catch(() => ({ healthy: false }))
}
export function StatusPopover() {
const sync = useSync()
const sdk = useSDK()
const server = useServer()
const platform = usePlatform()
const dialog = useDialog()
const language = useLanguage()
const navigate = useNavigate()
const [loading, setLoading] = createSignal<string | null>(null)
const [store, setStore] = createStore({
status: {} as Record<string, ServerStatus | undefined>,
})
const servers = createMemo(() => {
const current = server.url
const list = server.list
if (!current) return list
if (!list.includes(current)) return [current, ...list]
return [current, ...list.filter((x) => x !== current)]
})
const sortedServers = createMemo(() => {
const list = servers()
if (!list.length) return list
const active = server.url
const order = new Map(list.map((url, index) => [url, index] as const))
const rank = (value?: ServerStatus) => {
if (value?.healthy === true) return 0
if (value?.healthy === false) return 2
return 1
}
return list.slice().sort((a, b) => {
if (a === active) return -1
if (b === active) return 1
const diff = rank(store.status[a]) - rank(store.status[b])
if (diff !== 0) return diff
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
})
})
async function refreshHealth() {
const results: Record<string, ServerStatus> = {}
await Promise.all(
servers().map(async (url) => {
results[url] = await checkHealth(url, platform)
}),
)
setStore("status", reconcile(results))
}
createEffect(() => {
servers()
refreshHealth()
const interval = setInterval(refreshHealth, 10_000)
onCleanup(() => clearInterval(interval))
})
const mcpItems = createMemo(() =>
Object.entries(sync.data.mcp ?? {})
.map(([name, status]) => ({ name, status: status.status }))
.sort((a, b) => a.name.localeCompare(b.name)),
)
const mcpConnected = createMemo(() => mcpItems().filter((i) => i.status === "connected").length)
const toggleMcp = async (name: string) => {
if (loading()) return
setLoading(name)
const status = sync.data.mcp[name]
if (status?.status === "connected") {
await sdk.client.mcp.disconnect({ name })
} else {
await sdk.client.mcp.connect({ name })
}
const result = await sdk.client.mcp.status()
if (result.data) sync.set("mcp", result.data)
setLoading(null)
}
const lspItems = createMemo(() => sync.data.lsp ?? [])
const lspCount = createMemo(() => lspItems().length)
const plugins = createMemo(() => sync.data.config.plugin ?? [])
const pluginCount = createMemo(() => plugins().length)
const overallHealthy = createMemo(() => {
const serverHealthy = server.healthy() === true
const anyMcpIssue = mcpItems().some((m) => m.status !== "connected" && m.status !== "disabled")
return serverHealthy && !anyMcpIssue
})
const serverCount = createMemo(() => sortedServers().length)
const [defaultServerUrl, setDefaultServerUrl] = createSignal<string | undefined>()
createEffect(() => {
const result = platform.getDefaultServerUrl?.()
if (result instanceof Promise) {
result.then((url) => setDefaultServerUrl(url ? normalizeServerUrl(url) : undefined))
return
}
if (result) setDefaultServerUrl(normalizeServerUrl(result))
})
return (
<Popover
triggerAs={Button}
triggerProps={{
variant: "ghost",
class:
"rounded-sm w-[75px] h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none data-[expanded]:bg-surface-raised-base-active",
style: { scale: 1 },
}}
trigger={
<div class="flex items-center gap-1.5">
<div
classList={{
"size-1.5 rounded-full": true,
"bg-icon-success-base": overallHealthy(),
"bg-icon-critical-base": !overallHealthy() && server.healthy() !== undefined,
"bg-border-weak-base": server.healthy() === undefined,
}}
/>
<span class="text-12-regular text-text-strong">{language.t("status.popover.trigger")}</span>
</div>
}
class="[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl"
gutter={6}
placement="bottom-end"
shift={-136}
>
<div
class="flex items-center gap-1 w-[360px] rounded-xl"
style={{ "box-shadow": "var(--shadow-lg-border-base)" }}
>
<Tabs
aria-label={language.t("status.popover.ariaLabel")}
class="tabs"
data-component="tabs"
data-active="servers"
defaultValue="servers"
variant="alt"
style={{
"background-color": "var(--background-strong)",
"border-radius": "12px",
overflow: "hidden",
}}
>
<Tabs.List
data-slot="tablist"
style={{
"background-color": "transparent",
"border-bottom": "none",
padding: "8px 16px 0",
gap: "16px",
height: "40px",
}}
>
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
{serverCount() > 0 ? `${serverCount()} ` : ""}
{language.t("status.popover.tab.servers")}
</Tabs.Trigger>
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
{mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
{language.t("status.popover.tab.mcp")}
</Tabs.Trigger>
<Tabs.Trigger value="lsp" data-slot="tab" class="text-12-regular">
{lspCount() > 0 ? `${lspCount()} ` : ""}
{language.t("status.popover.tab.lsp")}
</Tabs.Trigger>
<Tabs.Trigger value="plugins" data-slot="tab" class="text-12-regular">
{pluginCount() > 0 ? `${pluginCount()} ` : ""}
{language.t("status.popover.tab.plugins")}
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="servers">
<div class="flex flex-col px-2 pb-2">
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
<For each={sortedServers()}>
{(url) => {
const isActive = () => url === server.url
const isDefault = () => url === defaultServerUrl()
const status = () => store.status[url]
const isBlocked = () => status()?.healthy === false
const [truncated, setTruncated] = createSignal(false)
let nameRef: HTMLSpanElement | undefined
let versionRef: HTMLSpanElement | undefined
onMount(() => {
const check = () => {
const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
const versionTruncated = versionRef ? versionRef.scrollWidth > versionRef.clientWidth : false
setTruncated(nameTruncated || versionTruncated)
}
check()
window.addEventListener("resize", check)
onCleanup(() => window.removeEventListener("resize", check))
})
const tooltipValue = () => {
const name = serverDisplayName(url)
const version = status()?.version
return (
<span class="flex items-center gap-2">
<span>{name}</span>
<Show when={version}>
<span class="text-text-invert-base">{version}</span>
</Show>
</span>
)
}
return (
<Tooltip value={tooltipValue()} placement="top" inactive={!truncated()}>
<button
type="button"
class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
classList={{
"opacity-50": isBlocked(),
"hover:bg-surface-raised-base-hover": !isBlocked(),
"cursor-not-allowed": isBlocked(),
}}
aria-disabled={isBlocked()}
onClick={() => {
if (isBlocked()) return
server.setActive(url)
navigate("/")
}}
>
<div
classList={{
"size-1.5 rounded-full shrink-0": true,
"bg-icon-success-base": status()?.healthy === true,
"bg-icon-critical-base": status()?.healthy === false,
"bg-border-weak-base": status() === undefined,
}}
/>
<span ref={nameRef} class="text-14-regular text-text-base truncate">
{serverDisplayName(url)}
</span>
<Show when={status()?.version}>
<span ref={versionRef} class="text-12-regular text-text-weak truncate">
{status()?.version}
</span>
</Show>
<Show when={isDefault()}>
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
{language.t("common.default")}
</span>
</Show>
<div class="flex-1" />
<Show when={isActive()}>
<Icon name="check" size="small" class="text-icon-weak shrink-0" />
</Show>
</button>
</Tooltip>
)
}}
</For>
<Button
variant="secondary"
class="mt-3 self-start h-8 px-3 py-1.5"
onClick={() => dialog.show(() => <DialogSelectServer />)}
>
{language.t("status.popover.action.manageServers")}
</Button>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="mcp">
<div class="flex flex-col px-2 pb-2">
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
<Show
when={mcpItems().length > 0}
fallback={
<div class="text-14-regular text-text-base text-center my-auto">
{language.t("dialog.mcp.empty")}
</div>
}
>
<For each={mcpItems()}>
{(item) => {
const enabled = () => item.status === "connected"
return (
<button
type="button"
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
onClick={() => toggleMcp(item.name)}
disabled={loading() === item.name}
>
<div
classList={{
"size-1.5 rounded-full shrink-0": true,
"bg-icon-success-base": item.status === "connected",
"bg-icon-critical-base": item.status === "failed",
"bg-border-weak-base": item.status === "disabled",
"bg-icon-warning-base":
item.status === "needs_auth" || item.status === "needs_client_registration",
}}
/>
<span class="text-14-regular text-text-base truncate flex-1">{item.name}</span>
<div onClick={(event) => event.stopPropagation()}>
<Switch
checked={enabled()}
disabled={loading() === item.name}
onChange={() => toggleMcp(item.name)}
/>
</div>
</button>
)
}}
</For>
</Show>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="lsp">
<div class="flex flex-col px-2 pb-2">
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
<Show
when={lspItems().length > 0}
fallback={
<div class="text-14-regular text-text-base text-center my-auto">
{language.t("dialog.lsp.empty")}
</div>
}
>
<For each={lspItems()}>
{(item) => (
<div class="flex items-center gap-2 w-full px-2 py-1">
<div
classList={{
"size-1.5 rounded-full shrink-0": true,
"bg-icon-success-base": item.status === "connected",
"bg-icon-critical-base": item.status === "error",
}}
/>
<span class="text-14-regular text-text-base truncate">{item.name || item.id}</span>
</div>
)}
</For>
</Show>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="plugins">
<div class="flex flex-col px-2 pb-2">
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
<Show
when={plugins().length > 0}
fallback={
<div class="text-14-regular text-text-base text-center my-auto">
{(() => {
const value = language.t("dialog.plugins.empty")
const file = "opencode.json"
const parts = value.split(file)
if (parts.length === 1) return value
return (
<>
{parts[0]}
<code class="bg-surface-raised-base px-1.5 py-0.5 rounded-sm text-text-base">{file}</code>
{parts.slice(1).join(file)}
</>
)
})()}
</div>
}
>
<For each={plugins()}>
{(plugin) => (
<div class="flex items-center gap-2 w-full px-2 py-1">
<div class="size-1.5 rounded-full shrink-0 bg-icon-success-base" />
<span class="text-14-regular text-text-base truncate">{plugin}</span>
</div>
)}
</For>
</Show>
</div>
</div>
</Tabs.Content>
</Tabs>
</div>
</Popover>
)
}

View File

@@ -111,6 +111,8 @@ export const Terminal = (props: TerminalProps) => {
const mod = await import("ghostty-web")
ghostty = await mod.Ghostty.load()
const once = { value: false }
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
if (window.__OPENCODE__?.serverPassword) {
url.username = "opencode"
@@ -258,6 +260,8 @@ export const Terminal = (props: TerminalProps) => {
})
socket.addEventListener("error", (error) => {
if (disposed) return
if (once.value) return
once.value = true
console.error("WebSocket error:", error)
local.onConnectError?.(error)
})
@@ -266,6 +270,8 @@ export const Terminal = (props: TerminalProps) => {
// Normal closure (code 1000) means PTY process exited - server event handles cleanup
// For other codes (network issues, server restart), trigger error handler
if (event.code !== 1000) {
if (once.value) return
once.value = true
local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`))
}
})

View File

@@ -19,9 +19,6 @@ export function Titlebar() {
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
const reserve = createMemo(
() => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"),
)
const web = createMemo(() => platform.platform === "web")
const getWin = () => {
@@ -81,7 +78,7 @@ export function Titlebar() {
classList={{
"flex items-center w-full min-w-0": true,
"pl-2": !mac(),
"pr-2": !windows(),
"pr-6": !windows(),
}}
onMouseDown={drag}
data-tauri-drag-region
@@ -110,7 +107,7 @@ export function Titlebar() {
</div>
</Show>
<TooltipKeybind
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0"}
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
placement="bottom"
title={language.t("command.sidebar.toggle")}
keybind={command.keybind("sidebar.toggle")}
@@ -145,6 +142,7 @@ export function Titlebar() {
data-tauri-drag-region
/>
<Show when={windows()}>
<div class="w-6 shrink-0" />
<div data-tauri-decorum-tb class="flex flex-row" />
</Show>
</div>

View File

@@ -0,0 +1,147 @@
import { batch, createMemo, createRoot, createSignal, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useParams } from "@solidjs/router"
import { Persist, persisted } from "@/utils/persist"
import type { SelectedLineRange } from "@/context/file"
export type LineComment = {
id: string
file: string
selection: SelectedLineRange
comment: string
time: number
}
type CommentFocus = { file: string; id: string }
const WORKSPACE_KEY = "__workspace__"
const MAX_COMMENT_SESSIONS = 20
type CommentSession = ReturnType<typeof createCommentSession>
type CommentCacheEntry = {
value: CommentSession
dispose: VoidFunction
}
function createCommentSession(dir: string, id: string | undefined) {
const legacy = `${dir}/comments${id ? "/" + id : ""}.v1`
const [store, setStore, _, ready] = persisted(
Persist.scoped(dir, id, "comments", [legacy]),
createStore<{
comments: Record<string, LineComment[]>
}>({
comments: {},
}),
)
const [focus, setFocus] = createSignal<CommentFocus | null>(null)
const [active, setActive] = createSignal<CommentFocus | null>(null)
const list = (file: string) => store.comments[file] ?? []
const add = (input: Omit<LineComment, "id" | "time">) => {
const next: LineComment = {
id: crypto.randomUUID(),
time: Date.now(),
...input,
}
batch(() => {
setStore("comments", input.file, (items) => [...(items ?? []), next])
setFocus({ file: input.file, id: next.id })
})
return next
}
const remove = (file: string, id: string) => {
setStore("comments", file, (items) => (items ?? []).filter((x) => x.id !== id))
setFocus((current) => (current?.id === id ? null : current))
}
const all = createMemo(() => {
const files = Object.keys(store.comments)
const items = files.flatMap((file) => store.comments[file] ?? [])
return items.slice().sort((a, b) => a.time - b.time)
})
return {
ready,
list,
all,
add,
remove,
focus: createMemo(() => focus()),
setFocus,
clearFocus: () => setFocus(null),
active: createMemo(() => active()),
setActive,
clearActive: () => setActive(null),
}
}
export const { use: useComments, provider: CommentsProvider } = createSimpleContext({
name: "Comments",
gate: false,
init: () => {
const params = useParams()
const cache = new Map<string, CommentCacheEntry>()
const disposeAll = () => {
for (const entry of cache.values()) {
entry.dispose()
}
cache.clear()
}
onCleanup(disposeAll)
const prune = () => {
while (cache.size > MAX_COMMENT_SESSIONS) {
const first = cache.keys().next().value
if (!first) return
const entry = cache.get(first)
entry?.dispose()
cache.delete(first)
}
}
const load = (dir: string, id: string | undefined) => {
const key = `${dir}:${id ?? WORKSPACE_KEY}`
const existing = cache.get(key)
if (existing) {
cache.delete(key)
cache.set(key, existing)
return existing.value
}
const entry = createRoot((dispose) => ({
value: createCommentSession(dir, id),
dispose,
}))
cache.set(key, entry)
prune()
return entry.value
}
const session = createMemo(() => load(params.dir!, params.id))
return {
ready: () => session().ready(),
list: (file: string) => session().list(file),
all: () => session().all(),
add: (input: Omit<LineComment, "id" | "time">) => session().add(input),
remove: (file: string, id: string) => session().remove(file, id),
focus: () => session().focus(),
setFocus: (focus: CommentFocus | null) => session().setFocus(focus),
clearFocus: () => session().clearFocus(),
active: () => session().active(),
setActive: (active: CommentFocus | null) => session().setActive(active),
clearActive: () => session().clearActive(),
}
},
})

View File

@@ -189,26 +189,15 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
const params = useParams()
const language = useLanguage()
const scope = createMemo(() => sdk.directory)
const directory = createMemo(() => sync.data.path.directory)
function normalize(input: string) {
const root = directory()
const prefix = root.endsWith("/") ? root : root + "/"
let path = input
// Only strip protocol and decode if it's a file URI
if (path.startsWith("file://")) {
const raw = stripQueryAndHash(stripFileProtocol(path))
try {
// Attempt to treat as a standard URI
path = decodeURIComponent(raw)
} catch {
// Fallback for legacy paths that might contain invalid URI sequences (e.g. "100%")
// In this case, we treat the path as raw, but still strip the protocol
path = raw
}
}
let path = stripQueryAndHash(stripFileProtocol(input))
if (path.startsWith(prefix)) {
path = path.slice(prefix.length)
@@ -231,8 +220,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
function tab(input: string) {
const path = normalize(input)
const encoded = path.split("/").map(encodeURIComponent).join("/")
return `file://${encoded}`
return `file://${path}`
}
function pathFromTab(tabValue: string) {
@@ -248,6 +236,12 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
file: {},
})
createEffect(() => {
scope()
inflight.clear()
setStore("file", {})
})
const viewCache = new Map<string, ViewCacheEntry>()
const disposeViews = () => {
@@ -298,12 +292,16 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
const path = normalize(input)
if (!path) return Promise.resolve()
const directory = scope()
const key = `${directory}\n${path}`
const client = sdk.client
ensure(path)
const current = store.file[path]
if (!options?.force && current?.loaded) return Promise.resolve()
const pending = inflight.get(path)
const pending = inflight.get(key)
if (pending) return pending
setStore(
@@ -315,9 +313,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
}),
)
const promise = sdk.client.file
const promise = client.file
.read({ path })
.then((x) => {
if (scope() !== directory) return
setStore(
"file",
path,
@@ -329,6 +328,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
)
})
.catch((e) => {
if (scope() !== directory) return
setStore(
"file",
path,
@@ -344,10 +344,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
})
})
.finally(() => {
inflight.delete(path)
inflight.delete(key)
})
inflight.set(path, promise)
inflight.set(key, promise)
return promise
}

View File

@@ -24,6 +24,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
type Queued = { directory: string; payload: Event }
let queue: Array<Queued | undefined> = []
let buffer: Array<Queued | undefined> = []
const coalesced = new Map<string, number>()
let timer: ReturnType<typeof setTimeout> | undefined
let last = 0
@@ -41,10 +42,13 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
if (timer) clearTimeout(timer)
timer = undefined
if (queue.length === 0) return
const events = queue
queue = []
queue = buffer
buffer = events
queue.length = 0
coalesced.clear()
if (events.length === 0) return
last = Date.now()
batch(() => {
@@ -53,6 +57,8 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
emitter.emit(event.directory, event.payload)
}
})
buffer.length = 0
}
const schedule = () => {
@@ -61,10 +67,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
timer = setTimeout(flush, Math.max(0, 16 - elapsed))
}
const stop = () => {
flush()
}
void (async () => {
const events = await eventSdk.global.event()
let yielded = Date.now()
@@ -87,12 +89,12 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
await new Promise<void>((resolve) => setTimeout(resolve, 0))
}
})()
.finally(stop)
.finally(flush)
.catch(() => undefined)
onCleanup(() => {
abort.abort()
stop()
flush()
})
const sdk = createOpencodeClient({

View File

@@ -28,6 +28,7 @@ import {
batch,
createContext,
createEffect,
untrack,
getOwner,
runWithOwner,
useContext,
@@ -44,11 +45,24 @@ import { usePlatform } from "./platform"
import { useLanguage } from "@/context/language"
import { Persist, persisted } from "@/utils/persist"
type ProjectMeta = {
name?: string
icon?: {
override?: string
color?: string
}
commands?: {
start?: string
}
}
type State = {
status: "loading" | "partial" | "complete"
agent: Agent[]
command: Command[]
project: string
projectMeta: ProjectMeta | undefined
icon: string | undefined
provider: ProviderListResponse
config: Config
path: Path
@@ -89,10 +103,32 @@ type VcsCache = {
ready: Accessor<boolean>
}
type MetaCache = {
store: Store<{ value: ProjectMeta | undefined }>
setStore: SetStoreFunction<{ value: ProjectMeta | undefined }>
ready: Accessor<boolean>
}
type IconCache = {
store: Store<{ value: string | undefined }>
setStore: SetStoreFunction<{ value: string | undefined }>
ready: Accessor<boolean>
}
type ChildOptions = {
bootstrap?: boolean
}
function normalizeProviderList(input: ProviderListResponse): ProviderListResponse {
return {
...input,
all: input.all.map((provider) => ({
...provider,
models: Object.fromEntries(Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated")),
})),
}
}
function createGlobalSync() {
const globalSDK = useGlobalSDK()
const platform = usePlatform()
@@ -100,6 +136,40 @@ function createGlobalSync() {
const owner = getOwner()
if (!owner) throw new Error("GlobalSync must be created within owner")
const vcsCache = new Map<string, VcsCache>()
const metaCache = new Map<string, MetaCache>()
const iconCache = new Map<string, IconCache>()
const sdkCache = new Map<string, ReturnType<typeof createOpencodeClient>>()
const sdkFor = (directory: string) => {
const cached = sdkCache.get(directory)
if (cached) return cached
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
fetch: platform.fetch,
directory,
throwOnError: true,
})
sdkCache.set(directory, sdk)
return sdk
}
const [projectCache, setProjectCache, , projectCacheReady] = persisted(
Persist.global("globalSync.project", ["globalSync.project.v1"]),
createStore({ value: [] as Project[] }),
)
const sanitizeProject = (project: Project) => {
if (!project.icon?.url && !project.icon?.override) return project
return {
...project,
icon: {
...project.icon,
url: undefined,
override: undefined,
},
}
}
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
error?: InitError
@@ -112,7 +182,7 @@ function createGlobalSync() {
}>({
ready: false,
path: { state: "", config: "", worktree: "", directory: "", home: "" },
project: [],
project: projectCache.value,
provider: { all: [], connected: [], default: {} },
provider_auth: {},
config: {},
@@ -120,7 +190,25 @@ function createGlobalSync() {
})
let bootstrapQueue: string[] = []
createEffect(async () => {
createEffect(() => {
if (!projectCacheReady()) return
if (globalStore.project.length !== 0) return
const cached = projectCache.value
if (cached.length === 0) return
setGlobalStore("project", cached)
})
createEffect(() => {
if (!projectCacheReady()) return
const projects = globalStore.project
if (projects.length === 0) {
const cachedLength = untrack(() => projectCache.value.length)
if (cachedLength !== 0) return
}
setProjectCache("value", projects.map(sanitizeProject))
})
createEffect(() => {
if (globalStore.reload !== "complete") return
if (bootstrapQueue.length) {
for (const directory of bootstrapQueue) {
@@ -140,18 +228,40 @@ function createGlobalSync() {
function ensureChild(directory: string) {
if (!directory) console.error("No directory provided")
if (!children[directory]) {
const cache = runWithOwner(owner, () =>
const vcs = runWithOwner(owner, () =>
persisted(
Persist.workspace(directory, "vcs", ["vcs.v1"]),
createStore({ value: undefined as VcsInfo | undefined }),
),
)
if (!cache) throw new Error("Failed to create persisted cache")
vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] })
if (!vcs) throw new Error("Failed to create persisted cache")
const vcsStore = vcs[0]
const vcsReady = vcs[3]
vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcsReady })
const meta = runWithOwner(owner, () =>
persisted(
Persist.workspace(directory, "project", ["project.v1"]),
createStore({ value: undefined as ProjectMeta | undefined }),
),
)
if (!meta) throw new Error("Failed to create persisted project metadata")
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
const icon = runWithOwner(owner, () =>
persisted(
Persist.workspace(directory, "icon", ["icon.v1"]),
createStore({ value: undefined as string | undefined }),
),
)
if (!icon) throw new Error("Failed to create persisted project icon")
iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
const init = () => {
children[directory] = createStore<State>({
const child = createStore<State>({
project: "",
projectMeta: meta[0].value,
icon: icon[0].value,
provider: { all: [], connected: [], default: {} },
config: {},
path: { state: "", config: "", worktree: "", directory: "", home: "" },
@@ -167,11 +277,28 @@ function createGlobalSync() {
question: {},
mcp: {},
lsp: [],
vcs: cache[0].value,
vcs: vcsStore.value,
limit: 5,
message: {},
part: {},
})
children[directory] = child
createEffect(() => {
if (!vcsReady()) return
const cached = vcsStore.value
if (!cached?.branch) return
child[1]("vcs", (value) => value ?? cached)
})
createEffect(() => {
child[1]("projectMeta", meta[0].value)
})
createEffect(() => {
child[1]("icon", icon[0].value)
})
}
runWithOwner(owner, init)
@@ -204,7 +331,6 @@ function createGlobalSync() {
const nonArchived = (x.data ?? [])
.filter((s) => !!s?.id)
.filter((s) => !s.time?.archived)
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
// Read the current limit at resolve-time so callers that bump the limit while
@@ -253,36 +379,20 @@ function createGlobalSync() {
const [store, setStore] = ensureChild(directory)
const cache = vcsCache.get(directory)
if (!cache) return
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
fetch: platform.fetch,
directory,
throwOnError: true,
})
const meta = metaCache.get(directory)
if (!meta) return
const sdk = sdkFor(directory)
setStore("status", "loading")
createEffect(() => {
if (!cache.ready()) return
const cached = cache.store.value
if (!cached?.branch) return
setStore("vcs", (value) => value ?? cached)
})
// projectMeta is synced from persisted storage in ensureChild.
// vcs is seeded from persisted storage in ensureChild.
const blockingRequests = {
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
provider: () =>
sdk.provider.list().then((x) => {
const data = x.data!
setStore("provider", {
...data,
all: data.all.map((provider) => ({
...provider,
models: Object.fromEntries(
Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"),
),
})),
})
setStore("provider", normalizeProviderList(x.data!))
}),
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
@@ -335,10 +445,7 @@ function createGlobalSync() {
"permission",
sessionID,
reconcile(
permissions
.filter((p) => !!p?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id)),
permissions.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id)),
{ key: "id" },
),
)
@@ -367,10 +474,7 @@ function createGlobalSync() {
"question",
sessionID,
reconcile(
questions
.filter((q) => !!q?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id)),
questions.filter((q) => !!q?.id).sort((a, b) => a.id.localeCompare(b.id)),
{ key: "id" },
),
)
@@ -475,6 +579,20 @@ function createGlobalSync() {
)
break
}
case "session.deleted": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (result.found) {
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
if (event.properties.info.parentID) break
setStore("sessionTotal", (value) => Math.max(0, value - 1))
break
}
case "session.diff":
setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" }))
break
@@ -639,13 +757,9 @@ function createGlobalSync() {
break
}
case "lsp.updated": {
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
fetch: platform.fetch,
directory,
throwOnError: true,
})
sdk.lsp.status().then((x) => setStore("lsp", x.data ?? []))
sdkFor(directory)
.lsp.status()
.then((x) => setStore("lsp", x.data ?? []))
break
}
}
@@ -685,16 +799,7 @@ function createGlobalSync() {
),
retry(() =>
globalSDK.client.provider.list().then((x) => {
const data = x.data!
setGlobalStore("provider", {
...data,
all: data.all.map((provider) => ({
...provider,
models: Object.fromEntries(
Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"),
),
})),
})
setGlobalStore("provider", normalizeProviderList(x.data!))
}),
),
retry(() =>
@@ -711,6 +816,32 @@ function createGlobalSync() {
bootstrap()
})
function projectMeta(directory: string, patch: ProjectMeta) {
const [store, setStore] = ensureChild(directory)
const cached = metaCache.get(directory)
if (!cached) return
const previous = store.projectMeta ?? {}
const icon = patch.icon ? { ...(previous.icon ?? {}), ...patch.icon } : previous.icon
const commands = patch.commands ? { ...(previous.commands ?? {}), ...patch.commands } : previous.commands
const next = {
...previous,
...patch,
icon,
commands,
}
cached.setStore("value", next)
setStore("projectMeta", next)
}
function projectIcon(directory: string, value: string | undefined) {
const [store, setStore] = ensureChild(directory)
const cached = iconCache.get(directory)
if (!cached) return
if (store.icon === value) return
cached.setStore("value", value)
setStore("icon", value)
}
return {
data: globalStore,
set: setGlobalStore,
@@ -732,6 +863,8 @@ function createGlobalSync() {
},
project: {
loadSessions,
meta: projectMeta,
icon: projectIcon,
},
}
}

View File

@@ -5,27 +5,39 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { Persist, persisted } from "@/utils/persist"
import { dict as en } from "@/i18n/en"
import { dict as zh } from "@/i18n/zh"
import { dict as zht } from "@/i18n/zht"
import { dict as ko } from "@/i18n/ko"
import { dict as de } from "@/i18n/de"
import { dict as es } from "@/i18n/es"
import { dict as fr } from "@/i18n/fr"
import { dict as da } from "@/i18n/da"
import { dict as ja } from "@/i18n/ja"
import { dict as pl } from "@/i18n/pl"
import { dict as ru } from "@/i18n/ru"
import { dict as ar } from "@/i18n/ar"
import { dict as no } from "@/i18n/no"
import { dict as br } from "@/i18n/br"
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht"
import { dict as uiKo } from "@opencode-ai/ui/i18n/ko"
import { dict as uiDe } from "@opencode-ai/ui/i18n/de"
import { dict as uiEs } from "@opencode-ai/ui/i18n/es"
import { dict as uiFr } from "@opencode-ai/ui/i18n/fr"
import { dict as uiDa } from "@opencode-ai/ui/i18n/da"
import { dict as uiJa } from "@opencode-ai/ui/i18n/ja"
import { dict as uiPl } from "@opencode-ai/ui/i18n/pl"
import { dict as uiRu } from "@opencode-ai/ui/i18n/ru"
import { dict as uiAr } from "@opencode-ai/ui/i18n/ar"
import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
import { dict as uiBr } from "@opencode-ai/ui/i18n/br"
export type Locale = "en" | "zh" | "ko" | "de" | "es" | "fr" | "da" | "ja"
export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br"
type RawDictionary = typeof en & typeof uiEn
type Dictionary = i18n.Flatten<RawDictionary>
const LOCALES: readonly Locale[] = ["en", "zh", "ko", "de", "es", "fr", "da", "ja"]
const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"]
function detectLocale(): Locale {
if (typeof navigator !== "object") return "en"
@@ -33,13 +45,26 @@ function detectLocale(): Locale {
const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
for (const language of languages) {
if (!language) continue
if (language.toLowerCase().startsWith("zh")) return "zh"
if (language.toLowerCase().startsWith("zh")) {
if (language.toLowerCase().includes("hant")) return "zht"
return "zh"
}
if (language.toLowerCase().startsWith("ko")) return "ko"
if (language.toLowerCase().startsWith("de")) return "de"
if (language.toLowerCase().startsWith("es")) return "es"
if (language.toLowerCase().startsWith("fr")) return "fr"
if (language.toLowerCase().startsWith("da")) return "da"
if (language.toLowerCase().startsWith("ja")) return "ja"
if (language.toLowerCase().startsWith("pl")) return "pl"
if (language.toLowerCase().startsWith("ru")) return "ru"
if (language.toLowerCase().startsWith("ar")) return "ar"
if (
language.toLowerCase().startsWith("no") ||
language.toLowerCase().startsWith("nb") ||
language.toLowerCase().startsWith("nn")
)
return "no"
if (language.toLowerCase().startsWith("pt")) return "br"
}
return "en"
@@ -57,12 +82,18 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
const locale = createMemo<Locale>(() => {
if (store.locale === "zh") return "zh"
if (store.locale === "zht") return "zht"
if (store.locale === "ko") return "ko"
if (store.locale === "de") return "de"
if (store.locale === "es") return "es"
if (store.locale === "fr") return "fr"
if (store.locale === "da") return "da"
if (store.locale === "ja") return "ja"
if (store.locale === "pl") return "pl"
if (store.locale === "ru") return "ru"
if (store.locale === "ar") return "ar"
if (store.locale === "no") return "no"
if (store.locale === "br") return "br"
return "en"
})
@@ -76,11 +107,17 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
const dict = createMemo<Dictionary>(() => {
if (locale() === "en") return base
if (locale() === "zh") return { ...base, ...i18n.flatten({ ...zh, ...uiZh }) }
if (locale() === "zht") return { ...base, ...i18n.flatten({ ...zht, ...uiZht }) }
if (locale() === "de") return { ...base, ...i18n.flatten({ ...de, ...uiDe }) }
if (locale() === "es") return { ...base, ...i18n.flatten({ ...es, ...uiEs }) }
if (locale() === "fr") return { ...base, ...i18n.flatten({ ...fr, ...uiFr }) }
if (locale() === "da") return { ...base, ...i18n.flatten({ ...da, ...uiDa }) }
if (locale() === "ja") return { ...base, ...i18n.flatten({ ...ja, ...uiJa }) }
if (locale() === "pl") return { ...base, ...i18n.flatten({ ...pl, ...uiPl }) }
if (locale() === "ru") return { ...base, ...i18n.flatten({ ...ru, ...uiRu }) }
if (locale() === "ar") return { ...base, ...i18n.flatten({ ...ar, ...uiAr }) }
if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) }
if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) }
return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }
})
@@ -89,12 +126,18 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
const labelKey: Record<Locale, keyof Dictionary> = {
en: "language.en",
zh: "language.zh",
zht: "language.zht",
ko: "language.ko",
de: "language.de",
es: "language.es",
fr: "language.fr",
da: "language.da",
ja: "language.ja",
pl: "language.pl",
ru: "language.ru",
ar: "language.ar",
no: "language.no",
br: "language.br",
}
const label = (value: Locale) => t(labelKey[value])

View File

@@ -1,5 +1,5 @@
import { createStore, produce } from "solid-js/store"
import { batch, createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { batch, createEffect, createMemo, on, onCleanup, onMount, type Accessor } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
@@ -209,6 +209,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
})
const [colors, setColors] = createStore<Record<string, AvatarColorKey>>({})
const colorRequested = new Map<string, AvatarColorKey>()
function pickAvailableColor(used: Set<string>): AvatarColorKey {
const available = AVATAR_COLOR_KEYS.filter((c) => !used.has(c))
@@ -222,15 +223,38 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const metadata = projectID
? globalSync.data.project.find((x) => x.id === projectID)
: globalSync.data.project.find((x) => x.worktree === project.worktree)
return {
const local = childStore.projectMeta
const localOverride =
local?.name !== undefined ||
local?.commands?.start !== undefined ||
local?.icon?.override !== undefined ||
local?.icon?.color !== undefined
const base = {
...(metadata ?? {}),
...project,
icon: {
url: metadata?.icon?.url,
override: metadata?.icon?.override,
override: metadata?.icon?.override ?? childStore.icon,
color: metadata?.icon?.color,
},
}
const isGlobal = projectID === "global" || (metadata?.id === undefined && localOverride)
if (!isGlobal) return base
return {
...base,
id: base.id ?? "global",
name: local?.name,
commands: local?.commands,
icon: {
url: base.icon?.url,
override: local?.icon?.override,
color: local?.icon?.color,
},
}
}
const roots = createMemo(() => {
@@ -244,17 +268,36 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
return map
})
createEffect(() => {
const rootFor = (directory: string) => {
const map = roots()
if (map.size === 0) return
if (map.size === 0) return directory
const visited = new Set<string>()
const chain = [directory]
while (chain.length) {
const current = chain[chain.length - 1]
if (!current) return directory
const next = map.get(current)
if (!next) return current
if (visited.has(next)) return directory
visited.add(next)
chain.push(next)
}
return directory
}
createEffect(() => {
const projects = server.projects.list()
const seen = new Set(projects.map((project) => project.worktree))
batch(() => {
for (const project of projects) {
const root = map.get(project.worktree)
if (!root) continue
const root = rootFor(project.worktree)
if (root === project.worktree) continue
server.projects.close(project.worktree)
@@ -282,6 +325,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
createEffect(() => {
const projects = enriched()
if (projects.length === 0) return
if (!globalSync.ready) return
for (const project of projects) {
if (!project.id) continue
if (project.id === "global") continue
globalSync.project.icon(project.worktree, project.icon?.override)
}
})
createEffect(() => {
const projects = enriched()
if (projects.length === 0) return
for (const project of projects) {
if (project.icon?.color) colorRequested.delete(project.worktree)
}
const used = new Set<string>()
for (const project of projects) {
@@ -291,12 +350,29 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
for (const project of projects) {
if (project.icon?.color) continue
if (colors[project.worktree]) continue
const color = pickAvailableColor(used)
used.add(color)
setColors(project.worktree, color)
const worktree = project.worktree
const existing = colors[worktree]
const color = existing ?? pickAvailableColor(used)
if (!existing) {
used.add(color)
setColors(worktree, color)
}
if (!project.id) continue
void globalSdk.client.project.update({ projectID: project.id, directory: project.worktree, icon: { color } })
const requested = colorRequested.get(worktree)
if (requested === color) continue
colorRequested.set(worktree, color)
if (project.id === "global") {
globalSync.project.meta(worktree, { icon: { color } })
continue
}
void globalSdk.client.project
.update({ projectID: project.id, directory: worktree, icon: { color } })
.catch(() => {
if (colorRequested.get(worktree) === color) colorRequested.delete(worktree)
})
}
})
@@ -313,7 +389,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
projects: {
list,
open(directory: string) {
const root = roots().get(directory) ?? directory
const root = rootFor(directory)
if (server.projects.list().find((x) => x.worktree === root)) return
globalSync.project.loadSessions(root)
server.projects.open(root)
@@ -347,7 +423,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("sidebar", "width", width)
},
workspaces(directory: string) {
return createMemo(() => store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false)
return () => store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false
},
setWorkspaces(directory: string, value: boolean) {
setStore("sidebar", "workspaces", directory, value)
@@ -395,10 +471,24 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("mobileSidebar", "opened", (x) => !x)
},
},
view(sessionKey: string) {
touch(sessionKey)
scroll.seed(sessionKey)
const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} })
view(sessionKey: string | Accessor<string>) {
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
touch(key())
scroll.seed(key())
createEffect(
on(
key,
(value) => {
touch(value)
scroll.seed(value)
},
{ defer: true },
),
)
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
@@ -428,10 +518,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
return {
scroll(tab: string) {
return scroll.scroll(sessionKey, tab)
return scroll.scroll(key(), tab)
},
setScroll(tab: string, pos: SessionScroll) {
scroll.setScroll(sessionKey, tab, pos)
scroll.setScroll(key(), tab, pos)
},
terminal: {
opened: terminalOpened,
@@ -460,9 +550,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
review: {
open: createMemo(() => s().reviewOpen),
setOpen(open: string[]) {
const current = store.sessionView[sessionKey]
const session = key()
const current = store.sessionView[session]
if (!current) {
setStore("sessionView", sessionKey, {
setStore("sessionView", session, {
scroll: {},
reviewOpen: open,
})
@@ -470,93 +561,111 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
if (same(current.reviewOpen, open)) return
setStore("sessionView", sessionKey, "reviewOpen", open)
setStore("sessionView", session, "reviewOpen", open)
},
},
}
},
tabs(sessionKey: string) {
touch(sessionKey)
const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
tabs(sessionKey: string | Accessor<string>) {
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
touch(key())
createEffect(
on(
key,
(value) => {
touch(value)
},
{ defer: true },
),
)
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
return {
tabs,
active: createMemo(() => tabs().active),
all: createMemo(() => tabs().all),
setActive(tab: string | undefined) {
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: [], active: tab })
const session = key()
if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all: [], active: tab })
} else {
setStore("sessionTabs", sessionKey, "active", tab)
setStore("sessionTabs", session, "active", tab)
}
},
setAll(all: string[]) {
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all, active: undefined })
const session = key()
if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all, active: undefined })
} else {
setStore("sessionTabs", sessionKey, "all", all)
setStore("sessionTabs", session, "all", all)
}
},
async open(tab: string) {
const current = store.sessionTabs[sessionKey] ?? { all: [] }
const session = key()
const current = store.sessionTabs[session] ?? { all: [] }
if (tab === "review") {
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: [], active: tab })
if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all: [], active: tab })
return
}
setStore("sessionTabs", sessionKey, "active", tab)
setStore("sessionTabs", session, "active", tab)
return
}
if (tab === "context") {
const all = [tab, ...current.all.filter((x) => x !== tab)]
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all, active: tab })
if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all, active: tab })
return
}
setStore("sessionTabs", sessionKey, "all", all)
setStore("sessionTabs", sessionKey, "active", tab)
setStore("sessionTabs", session, "all", all)
setStore("sessionTabs", session, "active", tab)
return
}
if (!current.all.includes(tab)) {
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: [tab], active: tab })
if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all: [tab], active: tab })
return
}
setStore("sessionTabs", sessionKey, "all", [...current.all, tab])
setStore("sessionTabs", sessionKey, "active", tab)
setStore("sessionTabs", session, "all", [...current.all, tab])
setStore("sessionTabs", session, "active", tab)
return
}
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: current.all, active: tab })
if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all: current.all, active: tab })
return
}
setStore("sessionTabs", sessionKey, "active", tab)
setStore("sessionTabs", session, "active", tab)
},
close(tab: string) {
const current = store.sessionTabs[sessionKey]
const session = key()
const current = store.sessionTabs[session]
if (!current) return
const all = current.all.filter((x) => x !== tab)
batch(() => {
setStore("sessionTabs", sessionKey, "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", sessionKey, "active", next)
setStore("sessionTabs", session, "active", next)
})
},
move(tab: string, to: number) {
const current = store.sessionTabs[sessionKey]
const session = key()
const current = store.sessionTabs[session]
if (!current) return
const index = current.all.findIndex((f) => f === tab)
if (index === -1) return
setStore(
"sessionTabs",
sessionKey,
session,
"all",
produce((opened) => {
opened.splice(to, 0, opened.splice(index, 1)[0])

View File

@@ -1,5 +1,5 @@
import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createMemo, onCleanup } from "solid-js"
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "@opencode-ai/ui/context"
@@ -126,7 +126,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
)
const [ephemeral, setEphemeral] = createStore<{
model: Record<string, ModelKey>
model: Record<string, ModelKey | undefined>
}>({
model: {},
})
@@ -182,7 +182,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
const fallbackModel = createMemo(() => {
const fallbackModel = createMemo<ModelKey | undefined>(() => {
if (sync.data.config.model) {
const [providerID, modelID] = sync.data.config.model.split("/")
if (isModelValid({ providerID, modelID })) {
@@ -199,16 +199,21 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}
const defaults = providers.default()
for (const p of providers.connected()) {
if (p.id in providers.default()) {
return {
providerID: p.id,
modelID: providers.default()[p.id],
}
const configured = defaults[p.id]
if (configured) {
const key = { providerID: p.id, modelID: configured }
if (isModelValid(key)) return key
}
const first = Object.values(p.models)[0]
if (!first) continue
const key = { providerID: p.id, modelID: first.id }
if (isModelValid(key)) return key
}
throw new Error("No default model found")
return undefined
})
const current = createMemo(() => {
@@ -266,7 +271,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
batch(() => {
const currentAgent = agent.current()
if (currentAgent) setEphemeral("model", currentAgent.name, model ?? fallbackModel())
const next = model ?? fallbackModel()
if (currentAgent) setEphemeral("model", currentAgent.name, next)
if (model) updateVisibility(model, "show")
if (options?.recent && model) {
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
@@ -338,6 +344,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
})
const scope = createMemo(() => sdk.directory)
createEffect(() => {
scope()
setStore("node", {})
})
// const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
// const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
@@ -394,10 +406,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const relative = (path: string) => path.replace(sync.data.path.directory + "/", "")
const load = async (path: string) => {
const directory = scope()
const client = sdk.client
const relativePath = relative(path)
await sdk.client.file
await client.file
.read({ path: relativePath })
.then((x) => {
if (scope() !== directory) return
if (!store.node[relativePath]) return
setStore(
"node",
@@ -409,6 +424,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
)
})
.catch((e) => {
if (scope() !== directory) return
showToast({
variant: "error",
title: language.t("toast.file.loadFailed.title"),
@@ -453,9 +469,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
const list = async (path: string) => {
return sdk.client.file
const directory = scope()
const client = sdk.client
return client.file
.list({ path: path + "/" })
.then((x) => {
if (scope() !== directory) return
setStore(
"node",
produce((draft) => {

View File

@@ -46,6 +46,9 @@ export type Platform = {
/** Set the default server URL to use on app startup (desktop only) */
setDefaultServerUrl?(url: string | null): Promise<void>
/** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */
parseMarkdown?(markdown: string): Promise<string>
}
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({

View File

@@ -4,6 +4,7 @@ import { batch, createMemo, createRoot, onCleanup } from "solid-js"
import { useParams } from "@solidjs/router"
import type { FileSelection } from "@/context/file"
import { Persist, persisted } from "@/utils/persist"
import { checksum } from "@opencode-ai/util/encode"
interface PartBase {
content: string
@@ -41,6 +42,10 @@ export type FileContextItem = {
type: "file"
path: string
selection?: FileSelection
comment?: string
commentID?: string
commentOrigin?: "review" | "file"
preview?: string
}
export type ContextItem = FileContextItem
@@ -118,14 +123,12 @@ function createPromptSession(dir: string, id: string | undefined) {
prompt: Prompt
cursor?: number
context: {
activeTab: boolean
items: (ContextItem & { key: string })[]
}
}>({
prompt: clonePrompt(DEFAULT_PROMPT),
cursor: undefined,
context: {
activeTab: true,
items: [],
},
}),
@@ -135,7 +138,16 @@ function createPromptSession(dir: string, id: string | undefined) {
if (item.type !== "file") return item.type
const start = item.selection?.startLine
const end = item.selection?.endLine
return `${item.type}:${item.path}:${start}:${end}`
const key = `${item.type}:${item.path}:${start}:${end}`
if (item.commentID) {
return `${key}:c=${item.commentID}`
}
const comment = item.comment?.trim()
if (!comment) return key
const digest = checksum(comment) ?? comment
return `${key}:c=${digest.slice(0, 8)}`
}
return {
@@ -144,14 +156,7 @@ function createPromptSession(dir: string, id: string | undefined) {
cursor: createMemo(() => store.cursor),
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
context: {
activeTab: createMemo(() => store.context.activeTab),
items: createMemo(() => store.context.items),
addActive() {
setStore("context", "activeTab", true)
},
removeActive() {
setStore("context", "activeTab", false)
},
add(item: ContextItem) {
const key = keyForItem(item)
if (store.context.items.find((x) => x.key === key)) return
@@ -230,10 +235,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
cursor: () => session().cursor(),
dirty: () => session().dirty(),
context: {
activeTab: () => session().context.activeTab(),
items: () => session().context.items(),
addActive: () => session().context.addActive(),
removeActive: () => session().context.removeActive(),
add: (item: ContextItem) => session().context.add(item),
remove: (key: string) => session().context.remove(key),
},

View File

@@ -1,7 +1,7 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
import { createEffect, createMemo, onCleanup } from "solid-js"
import { useGlobalSDK } from "./global-sdk"
import { usePlatform } from "./platform"
@@ -10,22 +10,39 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
init: (props: { directory: string }) => {
const platform = usePlatform()
const globalSDK = useGlobalSDK()
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
fetch: platform.fetch,
directory: props.directory,
throwOnError: true,
})
const directory = createMemo(() => props.directory)
const client = createMemo(() =>
createOpencodeClient({
baseUrl: globalSDK.url,
fetch: platform.fetch,
directory: directory(),
throwOnError: true,
}),
)
const emitter = createGlobalEmitter<{
[key in Event["type"]]: Extract<Event, { type: key }>
}>()
const unsub = globalSDK.event.on(props.directory, (event) => {
emitter.emit(event.type, event)
createEffect(() => {
const unsub = globalSDK.event.on(directory(), (event) => {
emitter.emit(event.type, event)
})
onCleanup(unsub)
})
onCleanup(unsub)
return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url }
return {
get directory() {
return directory()
},
get client() {
return client()
},
event: emitter,
get url() {
return globalSDK.url
},
}
},
})

View File

@@ -60,16 +60,17 @@ const monoFallback =
const monoFonts: Record<string, string> = {
"ibm-plex-mono": `"IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"cascadia-code": `"Cascadia Code Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"fira-code": `"Fira Code Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
hack: `"Hack Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
inconsolata: `"Inconsolata Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"intel-one-mono": `"Intel One Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"jetbrains-mono": `"JetBrains Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"meslo-lgs": `"Meslo LGS Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"roboto-mono": `"Roboto Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"source-code-pro": `"Source Code Pro Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"ubuntu-mono": `"Ubuntu Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"cascadia-code": `"Cascadia Code Nerd Font", "Cascadia Code NF", "Cascadia Mono NF", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"fira-code": `"Fira Code Nerd Font", "FiraMono Nerd Font", "FiraMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
hack: `"Hack Nerd Font", "Hack Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
inconsolata: `"Inconsolata Nerd Font", "Inconsolata Nerd Font Mono","IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"intel-one-mono": `"Intel One Mono Nerd Font", "IntoneMono Nerd Font", "IntoneMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
iosevka: `"Iosevka Nerd Font", "Iosevka Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"jetbrains-mono": `"JetBrains Mono Nerd Font", "JetBrainsMono Nerd Font Mono", "JetBrainsMonoNL Nerd Font", "JetBrainsMonoNL Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"meslo-lgs": `"Meslo LGS Nerd Font", "MesloLGS Nerd Font", "MesloLGM Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"roboto-mono": `"Roboto Mono Nerd Font", "RobotoMono Nerd Font", "RobotoMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"source-code-pro": `"Source Code Pro Nerd Font", "SauceCodePro Nerd Font", "SauceCodePro Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
"ubuntu-mono": `"Ubuntu Mono Nerd Font", "UbuntuMono Nerd Font", "UbuntuMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
}
export function monoFontFamily(font: string | undefined) {

View File

@@ -7,13 +7,20 @@ import { useGlobalSync } from "./global-sync"
import { useSDK } from "./sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
const keyFor = (directory: string, id: string) => `${directory}\n${id}`
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
init: () => {
const globalSync = useGlobalSync()
const sdk = useSDK()
const [store, setStore] = globalSync.child(sdk.directory)
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
type Child = ReturnType<(typeof globalSync)["child"]>
type Store = Child[0]
type Setter = Child[1]
const current = createMemo(() => globalSync.child(sdk.directory))
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
const chunk = 400
const inflight = new Map<string, Promise<void>>()
const inflightDiff = new Map<string, Promise<void>>()
@@ -25,6 +32,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})
const getSession = (sessionID: string) => {
const store = current()[0]
const match = Binary.search(store.session, sessionID, (s) => s.id)
if (match.found) return store.session[match.index]
return undefined
@@ -35,66 +43,75 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
return Math.ceil(count / chunk) * chunk
}
const hydrateMessages = (sessionID: string) => {
if (meta.limit[sessionID] !== undefined) return
const hydrateMessages = (directory: string, store: Store, sessionID: string) => {
const key = keyFor(directory, sessionID)
if (meta.limit[key] !== undefined) return
const messages = store.message[sessionID]
if (!messages) return
const limit = limitFor(messages.length)
setMeta("limit", sessionID, limit)
setMeta("complete", sessionID, messages.length < limit)
setMeta("limit", key, limit)
setMeta("complete", key, messages.length < limit)
}
const loadMessages = async (sessionID: string, limit: number) => {
if (meta.loading[sessionID]) return
const loadMessages = async (input: {
directory: string
client: typeof sdk.client
setStore: Setter
sessionID: string
limit: number
}) => {
const key = keyFor(input.directory, input.sessionID)
if (meta.loading[key]) return
setMeta("loading", sessionID, true)
await retry(() => sdk.client.session.messages({ sessionID, limit }))
setMeta("loading", key, true)
await retry(() => input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }))
.then((messages) => {
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
const next = items
.map((x) => x.info)
.filter((m) => !!m?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
batch(() => {
setStore("message", sessionID, reconcile(next, { key: "id" }))
input.setStore("message", input.sessionID, reconcile(next, { key: "id" }))
for (const message of items) {
setStore(
input.setStore(
"part",
message.info.id,
reconcile(
message.parts
.filter((p) => !!p?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id)),
message.parts.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id)),
{ key: "id" },
),
)
}
setMeta("limit", sessionID, limit)
setMeta("complete", sessionID, next.length < limit)
setMeta("limit", key, input.limit)
setMeta("complete", key, next.length < input.limit)
})
})
.finally(() => {
setMeta("loading", sessionID, false)
setMeta("loading", key, false)
})
}
return {
data: store,
set: setStore,
get data() {
return current()[0]
},
get set(): Setter {
return current()[1]
},
get status() {
return store.status
return current()[0].status
},
get ready() {
return store.status !== "loading"
return current()[0].status !== "loading"
},
get project() {
const store = current()[0]
const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
if (match.found) return globalSync.data.project[match.index]
return undefined
@@ -116,7 +133,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
agent: input.agent,
model: input.model,
}
setStore(
current()[1](
produce((draft) => {
const messages = draft.message[input.sessionID]
if (!messages) {
@@ -125,28 +142,33 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const result = Binary.search(messages, input.messageID, (m) => m.id)
messages.splice(result.index, 0, message)
}
draft.part[input.messageID] = input.parts
.filter((p) => !!p?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
draft.part[input.messageID] = input.parts.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id))
}),
)
},
async sync(sessionID: string) {
const hasSession = getSession(sessionID) !== undefined
hydrateMessages(sessionID)
const directory = sdk.directory
const client = sdk.client
const [store, setStore] = globalSync.child(directory)
const hasSession = (() => {
const match = Binary.search(store.session, sessionID, (s) => s.id)
return match.found
})()
hydrateMessages(directory, store, sessionID)
const hasMessages = store.message[sessionID] !== undefined
if (hasSession && hasMessages) return
const pending = inflight.get(sessionID)
const key = keyFor(directory, sessionID)
const pending = inflight.get(key)
if (pending) return pending
const limit = meta.limit[sessionID] ?? chunk
const limit = meta.limit[key] ?? chunk
const sessionReq = hasSession
? Promise.resolve()
: retry(() => sdk.client.session.get({ sessionID })).then((session) => {
: retry(() => client.session.get({ sessionID })).then((session) => {
const data = session.data
if (!data) return
setStore(
@@ -162,83 +184,117 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
)
})
const messagesReq = hasMessages ? Promise.resolve() : loadMessages(sessionID, limit)
const messagesReq = hasMessages
? Promise.resolve()
: loadMessages({
directory,
client,
setStore,
sessionID,
limit,
})
const promise = Promise.all([sessionReq, messagesReq])
.then(() => {})
.finally(() => {
inflight.delete(sessionID)
inflight.delete(key)
})
inflight.set(sessionID, promise)
inflight.set(key, promise)
return promise
},
async diff(sessionID: string) {
const directory = sdk.directory
const client = sdk.client
const [store, setStore] = globalSync.child(directory)
if (store.session_diff[sessionID] !== undefined) return
const pending = inflightDiff.get(sessionID)
const key = keyFor(directory, sessionID)
const pending = inflightDiff.get(key)
if (pending) return pending
const promise = retry(() => sdk.client.session.diff({ sessionID }))
const promise = retry(() => client.session.diff({ sessionID }))
.then((diff) => {
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
})
.finally(() => {
inflightDiff.delete(sessionID)
inflightDiff.delete(key)
})
inflightDiff.set(sessionID, promise)
inflightDiff.set(key, promise)
return promise
},
async todo(sessionID: string) {
const directory = sdk.directory
const client = sdk.client
const [store, setStore] = globalSync.child(directory)
if (store.todo[sessionID] !== undefined) return
const pending = inflightTodo.get(sessionID)
const key = keyFor(directory, sessionID)
const pending = inflightTodo.get(key)
if (pending) return pending
const promise = retry(() => sdk.client.session.todo({ sessionID }))
const promise = retry(() => client.session.todo({ sessionID }))
.then((todo) => {
setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
})
.finally(() => {
inflightTodo.delete(sessionID)
inflightTodo.delete(key)
})
inflightTodo.set(sessionID, promise)
inflightTodo.set(key, promise)
return promise
},
history: {
more(sessionID: string) {
const store = current()[0]
const key = keyFor(sdk.directory, sessionID)
if (store.message[sessionID] === undefined) return false
if (meta.limit[sessionID] === undefined) return false
if (meta.complete[sessionID]) return false
if (meta.limit[key] === undefined) return false
if (meta.complete[key]) return false
return true
},
loading(sessionID: string) {
return meta.loading[sessionID] ?? false
const key = keyFor(sdk.directory, sessionID)
return meta.loading[key] ?? false
},
async loadMore(sessionID: string, count = chunk) {
if (meta.loading[sessionID]) return
if (meta.complete[sessionID]) return
const directory = sdk.directory
const client = sdk.client
const [, setStore] = globalSync.child(directory)
const key = keyFor(directory, sessionID)
if (meta.loading[key]) return
if (meta.complete[key]) return
const current = meta.limit[sessionID] ?? chunk
await loadMessages(sessionID, current + count)
const currentLimit = meta.limit[key] ?? chunk
await loadMessages({
directory,
client,
setStore,
sessionID,
limit: currentLimit + count,
})
},
},
fetch: async (count = 10) => {
const directory = sdk.directory
const client = sdk.client
const [store, setStore] = globalSync.child(directory)
setStore("limit", (x) => x + count)
await sdk.client.session.list().then((x) => {
await client.session.list().then((x) => {
const sessions = (x.data ?? [])
.filter((s) => !!s?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
.slice(0, store.limit)
setStore("session", reconcile(sessions, { key: "id" }))
})
},
more: createMemo(() => store.session.length >= store.limit),
more: createMemo(() => current()[0].session.length >= current()[0].limit),
archive: async (sessionID: string) => {
await sdk.client.session.update({ sessionID, time: { archived: Date.now() } })
const directory = sdk.directory
const client = sdk.client
const [, setStore] = globalSync.child(directory)
await client.session.update({ sessionID, time: { archived: Date.now() } })
setStore(
produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id)
@@ -249,7 +305,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
absolute,
get directory() {
return store.path.directory
return current()[0].path.directory
},
}
},

View File

@@ -13,7 +13,6 @@ export type LocalPTY = {
cols?: number
buffer?: string
scrollY?: number
error?: boolean
}
const WORKSPACE_KEY = "__workspace__"
@@ -151,13 +150,18 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, sess
return undefined
})
if (!clone?.data) return
setStore("all", index, {
...pty,
...clone.data,
const active = store.active === pty.id
batch(() => {
setStore("all", index, {
...pty,
...clone.data,
})
if (active) {
setStore("active", clone.data.id)
}
})
if (store.active === pty.id) {
setStore("active", clone.data.id)
}
},
open(id: string) {
setStore("active", id)

674
packages/app/src/i18n/ar.ts Normal file
View File

@@ -0,0 +1,674 @@
export const dict = {
"command.category.suggested": "مقترح",
"command.category.view": "عرض",
"command.category.project": "مشروع",
"command.category.provider": "موفر",
"command.category.server": "خادم",
"command.category.session": "جلسة",
"command.category.theme": "سمة",
"command.category.language": "لغة",
"command.category.file": "ملف",
"command.category.terminal": "محطة طرفية",
"command.category.model": "نموذج",
"command.category.mcp": "MCP",
"command.category.agent": "وكيل",
"command.category.permissions": "أذونات",
"command.category.workspace": "مساحة عمل",
"command.category.settings": "إعدادات",
"theme.scheme.system": "نظام",
"theme.scheme.light": "فاتح",
"theme.scheme.dark": "داكن",
"command.sidebar.toggle": "تبديل الشريط الجانبي",
"command.project.open": "فتح مشروع",
"command.provider.connect": "اتصال بموفر",
"command.server.switch": "تبديل الخادم",
"command.settings.open": "فتح الإعدادات",
"command.session.previous": "الجلسة السابقة",
"command.session.next": "الجلسة التالية",
"command.session.archive": "أرشفة الجلسة",
"command.palette": "لوحة الأوامر",
"command.theme.cycle": "تغيير السمة",
"command.theme.set": "استخدام السمة: {{theme}}",
"command.theme.scheme.cycle": "تغيير مخطط الألوان",
"command.theme.scheme.set": "استخدام مخطط الألوان: {{scheme}}",
"command.language.cycle": "تغيير اللغة",
"command.language.set": "استخدام اللغة: {{language}}",
"command.session.new": "جلسة جديدة",
"command.file.open": "فتح ملف",
"command.file.open.description": "البحث في الملفات والأوامر",
"command.terminal.toggle": "تبديل المحطة الطرفية",
"command.review.toggle": "تبديل المراجعة",
"command.terminal.new": "محطة طرفية جديدة",
"command.terminal.new.description": "إنشاء علامة تبويب جديدة للمحطة الطرفية",
"command.steps.toggle": "تبديل الخطوات",
"command.steps.toggle.description": "إظهار أو إخفاء خطوات الرسالة الحالية",
"command.message.previous": "الرسالة السابقة",
"command.message.previous.description": "انتقل إلى رسالة المستخدم السابقة",
"command.message.next": "الرسالة التالية",
"command.message.next.description": "انتقل إلى رسالة المستخدم التالية",
"command.model.choose": "اختيار نموذج",
"command.model.choose.description": "حدد نموذجًا مختلفًا",
"command.mcp.toggle": "تبديل MCPs",
"command.mcp.toggle.description": "تبديل MCPs",
"command.agent.cycle": "تغيير الوكيل",
"command.agent.cycle.description": "التبديل إلى الوكيل التالي",
"command.agent.cycle.reverse": "تغيير الوكيل للخلف",
"command.agent.cycle.reverse.description": "التبديل إلى الوكيل السابق",
"command.model.variant.cycle": "تغيير جهد التفكير",
"command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
"command.session.undo": "تراجع",
"command.session.undo.description": "تراجع عن الرسالة الأخيرة",
"command.session.redo": "إعادة",
"command.session.redo.description": "إعادة الرسالة التي تم التراجع عنها",
"command.session.compact": "ضغط الجلسة",
"command.session.compact.description": "تلخيص الجلسة لتقليل حجم السياق",
"command.session.fork": "تشعب من الرسالة",
"command.session.fork.description": "إنشاء جلسة جديدة من رسالة سابقة",
"command.session.share": "مشاركة الجلسة",
"command.session.share.description": "مشاركة هذه الجلسة ونسخ الرابط إلى الحافظة",
"command.session.unshare": "إلغاء مشاركة الجلسة",
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
"palette.search.placeholder": "البحث في الملفات والأوامر",
"palette.empty": "لا توجد نتائج",
"palette.group.commands": "الأوامر",
"palette.group.files": "الملفات",
"dialog.provider.search.placeholder": "البحث عن موفرين",
"dialog.provider.empty": "لم يتم العثور على موفرين",
"dialog.provider.group.popular": "شائع",
"dialog.provider.group.other": "آخر",
"dialog.provider.tag.recommended": "موصى به",
"dialog.provider.anthropic.note": "اتصل باستخدام Claude Pro/Max أو مفتاح API",
"dialog.provider.openai.note": "اتصل باستخدام ChatGPT Pro/Plus أو مفتاح API",
"dialog.provider.copilot.note": "اتصل باستخدام Copilot أو مفتاح API",
"dialog.model.select.title": "تحديد نموذج",
"dialog.model.search.placeholder": "البحث عن نماذج",
"dialog.model.empty": "لا توجد نتائج للنماذج",
"dialog.model.manage": "إدارة النماذج",
"dialog.model.manage.description": "تخصيص النماذج التي تظهر في محدد النماذج.",
"dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من OpenCode",
"dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين",
"dialog.provider.viewAll": "عرض المزيد من الموفرين",
"provider.connect.title": "اتصال {{provider}}",
"provider.connect.title.anthropicProMax": "تسجيل الدخول باستخدام Claude Pro/Max",
"provider.connect.selectMethod": "حدد طريقة تسجيل الدخول لـ {{provider}}.",
"provider.connect.method.apiKey": "مفتاح API",
"provider.connect.status.inProgress": "جارٍ التفويض...",
"provider.connect.status.waiting": "في انتظار التفويض...",
"provider.connect.status.failed": "فشل التفويض: {{error}}",
"provider.connect.apiKey.description":
"أدخل مفتاح واجهة برمجة تطبيقات {{provider}} الخاص بك لتوصيل حسابك واستخدام نماذج {{provider}} في OpenCode.",
"provider.connect.apiKey.label": "مفتاح واجهة برمجة تطبيقات {{provider}}",
"provider.connect.apiKey.placeholder": "مفتاح API",
"provider.connect.apiKey.required": "مفتاح API مطلوب",
"provider.connect.opencodeZen.line1":
"يمنحك OpenCode Zen الوصول إلى مجموعة مختارة من النماذج الموثوقة والمحسنة لوكلاء البرمجة.",
"provider.connect.opencodeZen.line2":
"باستخدام مفتاح API واحد، ستحصل على إمكانية الوصول إلى نماذج مثل Claude و GPT و Gemini و GLM والمزيد.",
"provider.connect.opencodeZen.visit.prefix": "قم بزيارة ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": " للحصول على مفتاح API الخاص بك.",
"provider.connect.oauth.code.visit.prefix": "قم بزيارة ",
"provider.connect.oauth.code.visit.link": "هذا الرابط",
"provider.connect.oauth.code.visit.suffix":
" للحصول على رمز التفويض الخاص بك لتوصيل حسابك واستخدام نماذج {{provider}} في OpenCode.",
"provider.connect.oauth.code.label": "رمز تفويض {{method}}",
"provider.connect.oauth.code.placeholder": "رمز التفويض",
"provider.connect.oauth.code.required": "رمز التفويض مطلوب",
"provider.connect.oauth.code.invalid": "رمز التفويض غير صالح",
"provider.connect.oauth.auto.visit.prefix": "قم بزيارة ",
"provider.connect.oauth.auto.visit.link": "هذا الرابط",
"provider.connect.oauth.auto.visit.suffix":
" وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في OpenCode.",
"provider.connect.oauth.auto.confirmationCode": "رمز التأكيد",
"provider.connect.toast.connected.title": "تم توصيل {{provider}}",
"provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.",
"model.tag.free": "مجاني",
"model.tag.latest": "الأحدث",
"model.provider.anthropic": "Anthropic",
"model.provider.openai": "OpenAI",
"model.provider.google": "Google",
"model.provider.xai": "xAI",
"model.provider.meta": "Meta",
"model.input.text": "نص",
"model.input.image": "صورة",
"model.input.audio": "صوت",
"model.input.video": "فيديو",
"model.input.pdf": "pdf",
"model.tooltip.allows": "يسمح: {{inputs}}",
"model.tooltip.reasoning.allowed": "يسمح بالاستنتاج",
"model.tooltip.reasoning.none": "بدون استنتاج",
"model.tooltip.context": "حد السياق {{limit}}",
"common.search.placeholder": "بحث",
"common.goBack": "رجوع",
"common.loading": "جارٍ التحميل",
"common.loading.ellipsis": "...",
"common.cancel": "إلغاء",
"common.submit": "إرسال",
"common.save": "حفظ",
"common.saving": "جارٍ الحفظ...",
"common.default": "افتراضي",
"common.attachment": "مرفق",
"prompt.placeholder.shell": "أدخل أمر shell...",
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc للخروج",
"prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية",
"prompt.example.2": "ما هو المكدس التقني لهذا المشروع؟",
"prompt.example.3": "إصلاح الاختبارات المعطلة",
"prompt.example.4": "اشرح كيف تعمل المصادقة",
"prompt.example.5": "البحث عن وإصلاح الثغرات الأمنية",
"prompt.example.6": "إضافة اختبارات وحدة لخدمة المستخدم",
"prompt.example.7": "إعادة هيكلة هذه الدالة لتكون أكثر قابلية للقراءة",
"prompt.example.8": "ماذا يعني هذا الخطأ؟",
"prompt.example.9": "ساعدني في تصحيح هذه المشكلة",
"prompt.example.10": "توليد وثائق API",
"prompt.example.11": "تحسين استعلامات قاعدة البيانات",
"prompt.example.12": "إضافة التحقق من صحة الإدخال",
"prompt.example.13": "إنشاء مكون جديد لـ...",
"prompt.example.14": "كيف أقوم بنشر هذا المشروع؟",
"prompt.example.15": "مراجعة الكود الخاص بي لأفضل الممارسات",
"prompt.example.16": "إضافة معالجة الأخطاء لهذه الدالة",
"prompt.example.17": "اشرح نمط regex هذا",
"prompt.example.18": "تحويل هذا إلى TypeScript",
"prompt.example.19": "إضافة تسجيل الدخول (logging) في جميع أنحاء قاعدة التعليمات البرمجية",
"prompt.example.20": "ما هي التبعيات القديمة؟",
"prompt.example.21": "ساعدني في كتابة برنامج نصي للهجرة",
"prompt.example.22": "تنفيذ التخزين المؤقت لهذه النقطة النهائية",
"prompt.example.23": "إضافة ترقيم الصفحات إلى هذه القائمة",
"prompt.example.24": "إنشاء أمر CLI لـ...",
"prompt.example.25": "كيف تعمل متغيرات البيئة هنا؟",
"prompt.popover.emptyResults": "لا توجد نتائج مطابقة",
"prompt.popover.emptyCommands": "لا توجد أوامر مطابقة",
"prompt.dropzone.label": "أفلت الصور أو ملفات PDF هنا",
"prompt.slash.badge.custom": "مخصص",
"prompt.context.active": "نشط",
"prompt.context.includeActiveFile": "تضمين الملف النشط",
"prompt.context.removeActiveFile": "إزالة الملف النشط من السياق",
"prompt.context.removeFile": "إزالة الملف من السياق",
"prompt.action.attachFile": "إرفاق ملف",
"prompt.attachment.remove": "إزالة المرفق",
"prompt.action.send": "إرسال",
"prompt.action.stop": "توقف",
"prompt.toast.pasteUnsupported.title": "لصق غير مدعوم",
"prompt.toast.pasteUnsupported.description": "يمكن لصق الصور أو ملفات PDF فقط هنا.",
"prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً",
"prompt.toast.modelAgentRequired.description": "اختر وكيلاً ونموذجاً قبل إرسال الموجه.",
"prompt.toast.worktreeCreateFailed.title": "فشل إنشاء شجرة العمل",
"prompt.toast.sessionCreateFailed.title": "فشل إنشاء الجلسة",
"prompt.toast.shellSendFailed.title": "فشل إرسال أمر shell",
"prompt.toast.commandSendFailed.title": "فشل إرسال الأمر",
"prompt.toast.promptSendFailed.title": "فشل إرسال الموجه",
"dialog.mcp.title": "MCPs",
"dialog.mcp.description": "{{enabled}} من {{total}} مفعل",
"dialog.mcp.empty": "لم يتم تكوين MCPs",
"dialog.lsp.empty": "تم الكشف تلقائيًا عن LSPs من أنواع الملفات",
"dialog.plugins.empty": "الإضافات المكونة في opencode.json",
"mcp.status.connected": "متصل",
"mcp.status.failed": "فشل",
"mcp.status.needs_auth": "يحتاج إلى مصادقة",
"mcp.status.disabled": "معطل",
"dialog.fork.empty": "لا توجد رسائل للتفرع منها",
"dialog.directory.search.placeholder": "البحث في المجلدات",
"dialog.directory.empty": "لم يتم العثور على مجلدات",
"dialog.server.title": "الخوادم",
"dialog.server.description": "تبديل خادم OpenCode الذي يتصل به هذا التطبيق.",
"dialog.server.search.placeholder": "البحث في الخوادم",
"dialog.server.empty": "لا توجد خوادم بعد",
"dialog.server.add.title": "إضافة خادم",
"dialog.server.add.url": "عنوان URL للخادم",
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "تعذر الاتصال بالخادم",
"dialog.server.add.checking": "جارٍ التحقق...",
"dialog.server.add.button": "إضافة خادم",
"dialog.server.default.title": "الخادم الافتراضي",
"dialog.server.default.description":
"الاتصال بهذا الخادم عند بدء تشغيل التطبيق بدلاً من بدء خادم محلي. يتطلب إعادة التشغيل.",
"dialog.server.default.none": "لم يتم تحديد خادم",
"dialog.server.default.set": "تعيين الخادم الحالي كافتراضي",
"dialog.server.default.clear": "مسح",
"dialog.server.action.remove": "إزالة الخادم",
"dialog.server.menu.edit": "تعديل",
"dialog.server.menu.default": "تعيين كافتراضي",
"dialog.server.menu.defaultRemove": "إزالة الافتراضي",
"dialog.server.menu.delete": "حذف",
"dialog.server.current": "الخادم الحالي",
"dialog.server.status.default": "افتراضي",
"dialog.project.edit.title": "تحرير المشروع",
"dialog.project.edit.name": "الاسم",
"dialog.project.edit.icon": "أيقونة",
"dialog.project.edit.icon.alt": "أيقونة المشروع",
"dialog.project.edit.icon.hint": "انقر أو اسحب صورة",
"dialog.project.edit.icon.recommended": "موصى به: 128x128px",
"dialog.project.edit.color": "لون",
"dialog.project.edit.color.select": "اختر لون {{color}}",
"context.breakdown.title": "تفصيل السياق",
"context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال. يشمل "أخرى" تعريفات الأدوات والنفقات العامة.',
"context.breakdown.system": "النظام",
"context.breakdown.user": "المستخدم",
"context.breakdown.assistant": "المساعد",
"context.breakdown.tool": "استدعاءات الأداة",
"context.breakdown.other": "أخرى",
"context.systemPrompt.title": "موجه النظام",
"context.rawMessages.title": "الرسائل الخام",
"context.stats.session": "جلسة",
"context.stats.messages": "رسائل",
"context.stats.provider": "موفر",
"context.stats.model": "نموذج",
"context.stats.limit": "حد السياق",
"context.stats.totalTokens": "إجمالي الرموز",
"context.stats.usage": "استخدام",
"context.stats.inputTokens": "رموز الإدخال",
"context.stats.outputTokens": "رموز الإخراج",
"context.stats.reasoningTokens": "رموز الاستنتاج",
"context.stats.cacheTokens": "رموز التخزين المؤقت (قراءة/كتابة)",
"context.stats.userMessages": "رسائل المستخدم",
"context.stats.assistantMessages": "رسائل المساعد",
"context.stats.totalCost": "التكلفة الإجمالية",
"context.stats.sessionCreated": "تم إنشاء الجلسة",
"context.stats.lastActivity": "آخر نشاط",
"context.usage.tokens": "رموز",
"context.usage.usage": "استخدام",
"context.usage.cost": "تكلفة",
"context.usage.clickToView": "انقر لعرض السياق",
"context.usage.view": "عرض استخدام السياق",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"toast.language.title": "لغة",
"toast.language.description": "تم التبديل إلى {{language}}",
"toast.theme.title": "تم تبديل السمة",
"toast.scheme.title": "مخطط الألوان",
"toast.permissions.autoaccept.on.title": "قبول التعديلات تلقائيًا",
"toast.permissions.autoaccept.on.description": "سيتم الموافقة تلقائيًا على أذونات التحرير والكتابة",
"toast.permissions.autoaccept.off.title": "توقف قبول التعديلات تلقائيًا",
"toast.permissions.autoaccept.off.description": "ستتطلب أذونات التحرير والكتابة موافقة",
"toast.model.none.title": "لم يتم تحديد نموذج",
"toast.model.none.description": "قم بتوصيل موفر لتلخيص هذه الجلسة",
"toast.file.loadFailed.title": "فشل تحميل الملف",
"toast.session.share.copyFailed.title": "فشل نسخ عنوان URL إلى الحافظة",
"toast.session.share.success.title": "تمت مشاركة الجلسة",
"toast.session.share.success.description": "تم نسخ عنوان URL للمشاركة إلى الحافظة!",
"toast.session.share.failed.title": "فشل مشاركة الجلسة",
"toast.session.share.failed.description": "حدث خطأ أثناء مشاركة الجلسة",
"toast.session.unshare.success.title": "تم إلغاء مشاركة الجلسة",
"toast.session.unshare.success.description": "تم إلغاء مشاركة الجلسة بنجاح!",
"toast.session.unshare.failed.title": "فشل إلغاء مشاركة الجلسة",
"toast.session.unshare.failed.description": "حدث خطأ أثناء إلغاء مشاركة الجلسة",
"toast.session.listFailed.title": "فشل تحميل الجلسات لـ {{project}}",
"toast.update.title": "تحديث متاح",
"toast.update.description": "نسخة جديدة من OpenCode ({{version}}) متاحة الآن للتثبيت.",
"toast.update.action.installRestart": "تثبيت وإعادة تشغيل",
"toast.update.action.notYet": "ليس الآن",
"error.page.title": "حدث خطأ ما",
"error.page.description": "حدث خطأ أثناء تحميل التطبيق.",
"error.page.details.label": "تفاصيل الخطأ",
"error.page.action.restart": "إعادة تشغيل",
"error.page.action.checking": "جارٍ التحقق...",
"error.page.action.checkUpdates": "التحقق من وجود تحديثات",
"error.page.action.updateTo": "تحديث إلى {{version}}",
"error.page.report.prefix": "يرجى الإبلاغ عن هذا الخطأ لفريق OpenCode",
"error.page.report.discord": "على Discord",
"error.page.version": "الإصدار: {{version}}",
"error.dev.rootNotFound":
"لم يتم العثور على العنصر الجذري. هل نسيت إضافته إلى index.html؟ أو ربما تمت كتابة سمة id بشكل خاطئ؟",
"error.globalSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟",
"error.chain.unknown": "خطأ غير معروف",
"error.chain.causedBy": "بسبب:",
"error.chain.apiError": "خطأ API",
"error.chain.status": "الحالة: {{status}}",
"error.chain.retryable": "قابل لإعادة المحاولة: {{retryable}}",
"error.chain.responseBody": "نص الاستجابة:\n{{body}}",
"error.chain.didYouMean": "هل كنت تعني: {{suggestions}}",
"error.chain.modelNotFound": "النموذج غير موجود: {{provider}}/{{model}}",
"error.chain.checkConfig": "تحقق من أسماء الموفر/النموذج في التكوين (opencode.json)",
"error.chain.mcpFailed": 'فشل خادم MCP "{{name}}". لاحظ أن OpenCode لا يدعم مصادقة MCP بعد.',
"error.chain.providerAuthFailed": "فشلت مصادقة الموفر ({{provider}}): {{message}}",
"error.chain.providerInitFailed": 'فشل تهيئة الموفر "{{provider}}". تحقق من بيانات الاعتماد والتكوين.',
"error.chain.configJsonInvalid": "ملف التكوين في {{path}} ليس JSON(C) صالحًا",
"error.chain.configJsonInvalidWithMessage": "ملف التكوين في {{path}} ليس JSON(C) صالحًا: {{message}}",
"error.chain.configDirectoryTypo":
'الدليل "{{dir}}" في {{path}} غير صالح. أعد تسمية الدليل إلى "{{suggestion}}" أو قم بإزالته. هذا خطأ مطبعي شائع.',
"error.chain.configFrontmatterError": "فشل تحليل frontmatter في {{path}}:\n{{message}}",
"error.chain.configInvalid": "ملف التكوين في {{path}} غير صالح",
"error.chain.configInvalidWithMessage": "ملف التكوين في {{path}} غير صالح: {{message}}",
"notification.permission.title": "مطلوب إذن",
"notification.permission.description": "{{sessionTitle}} في {{projectName}} يحتاج إلى إذن",
"notification.question.title": "سؤال",
"notification.question.description": "{{sessionTitle}} في {{projectName}} لديه سؤال",
"notification.action.goToSession": "انتقل إلى الجلسة",
"notification.session.responseReady.title": "الاستجابة جاهزة",
"notification.session.error.title": "خطأ في الجلسة",
"notification.session.error.fallbackDescription": "حدث خطأ",
"home.recentProjects": "المشاريع الحديثة",
"home.empty.title": "لا توجد مشاريع حديثة",
"home.empty.description": "ابدأ بفتح مشروع محلي",
"session.tab.session": "جلسة",
"session.tab.review": "مراجعة",
"session.tab.context": "سياق",
"session.panel.reviewAndFiles": "المراجعة والملفات",
"session.review.filesChanged": "تم تغيير {{count}} ملفات",
"session.review.loadingChanges": "جارٍ تحميل التغييرات...",
"session.review.empty": "لا توجد تغييرات في هذه الجلسة بعد",
"session.messages.renderEarlier": "عرض الرسائل السابقة",
"session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...",
"session.messages.loadEarlier": "تحميل الرسائل السابقة",
"session.messages.loading": "جارٍ تحميل الرسائل...",
"session.messages.jumpToLatest": "الانتقال إلى الأحدث",
"session.context.addToContext": "إضافة {{selection}} إلى السياق",
"session.new.worktree.main": "الفرع الرئيسي",
"session.new.worktree.mainWithBranch": "الفرع الرئيسي ({{branch}})",
"session.new.worktree.create": "إنشاء شجرة عمل جديدة",
"session.new.lastModified": "آخر تعديل",
"session.header.search.placeholder": "بحث {{project}}",
"session.header.searchFiles": "بحث عن الملفات",
"status.popover.trigger": "الحالة",
"status.popover.ariaLabel": "إعدادات الخوادم",
"status.popover.tab.servers": "الخوادم",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "الإضافات",
"status.popover.action.manageServers": "إدارة الخوادم",
"session.share.popover.title": "نشر على الويب",
"session.share.popover.description.shared": "هذه الجلسة عامة على الويب. يمكن لأي شخص لديه الرابط الوصول إليها.",
"session.share.popover.description.unshared": "شارك الجلسة علنًا على الويب. ستكون متاحة لأي شخص لديه الرابط.",
"session.share.action.share": "مشاركة",
"session.share.action.publish": "نشر",
"session.share.action.publishing": "جارٍ النشر...",
"session.share.action.unpublish": "إلغاء النشر",
"session.share.action.unpublishing": "جارٍ إلغاء النشر...",
"session.share.action.view": "عرض",
"session.share.copy.copied": "تم النسخ",
"session.share.copy.copyLink": "نسخ الرابط",
"lsp.tooltip.none": "لا توجد خوادم LSP",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "جارٍ تحميل الموجه...",
"terminal.loading": "جارٍ تحميل المحطة الطرفية...",
"terminal.title": "محطة طرفية",
"terminal.title.numbered": "محطة طرفية {{number}}",
"terminal.close": "إغلاق المحطة الطرفية",
"terminal.connectionLost.title": "فقد الاتصال",
"terminal.connectionLost.description": "انقطع اتصال المحطة الطرفية. يمكن أن يحدث هذا عند إعادة تشغيل الخادم.",
"common.closeTab": "إغلاق علامة التبويب",
"common.dismiss": "رفض",
"common.requestFailed": "فشل الطلب",
"common.moreOptions": "مزيد من الخيارات",
"common.learnMore": "اعرف المزيد",
"common.rename": "إعادة تسمية",
"common.reset": "إعادة تعيين",
"common.archive": "أرشفة",
"common.delete": "حذف",
"common.close": "إغلاق",
"common.edit": "تحرير",
"common.loadMore": "تحميل المزيد",
"common.key.esc": "ESC",
"sidebar.menu.toggle": "تبديل القائمة",
"sidebar.nav.projectsAndSessions": "المشاريع والجلسات",
"sidebar.settings": "الإعدادات",
"sidebar.help": "مساعدة",
"sidebar.workspaces.enable": "تمكين مساحات العمل",
"sidebar.workspaces.disable": "تعطيل مساحات العمل",
"sidebar.gettingStarted.title": "البدء",
"sidebar.gettingStarted.line1": "يتضمن OpenCode نماذج مجانية حتى تتمكن من البدء فورًا.",
"sidebar.gettingStarted.line2": "قم بتوصيل أي موفر لاستخدام النماذج، بما في ذلك Claude و GPT و Gemini وما إلى ذلك.",
"sidebar.project.recentSessions": "الجلسات الحديثة",
"sidebar.project.viewAllSessions": "عرض جميع الجلسات",
"settings.section.desktop": "سطح المكتب",
"settings.tab.general": "عام",
"settings.tab.shortcuts": "اختصارات",
"settings.general.section.appearance": "المظهر",
"settings.general.section.notifications": "إشعارات النظام",
"settings.general.section.sounds": "المؤثرات الصوتية",
"settings.general.row.language.title": "اللغة",
"settings.general.row.language.description": "تغيير لغة العرض لـ OpenCode",
"settings.general.row.appearance.title": "المظهر",
"settings.general.row.appearance.description": "تخصيص كيفية ظهور OpenCode على جهازك",
"settings.general.row.theme.title": "السمة",
"settings.general.row.theme.description": "تخصيص سمة OpenCode.",
"settings.general.row.font.title": "الخط",
"settings.general.row.font.description": "تخصيص الخط الأحادي المستخدم في كتل التعليمات البرمجية",
"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.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": "تنبيه 01",
"sound.option.alert02": "تنبيه 02",
"sound.option.alert03": "تنبيه 03",
"sound.option.alert04": "تنبيه 04",
"sound.option.alert05": "تنبيه 05",
"sound.option.alert06": "تنبيه 06",
"sound.option.alert07": "تنبيه 07",
"sound.option.alert08": "تنبيه 08",
"sound.option.alert09": "تنبيه 09",
"sound.option.alert10": "تنبيه 10",
"sound.option.bipbop01": "بيب بوب 01",
"sound.option.bipbop02": "بيب بوب 02",
"sound.option.bipbop03": "بيب بوب 03",
"sound.option.bipbop04": "بيب بوب 04",
"sound.option.bipbop05": "بيب بوب 05",
"sound.option.bipbop06": "بيب بوب 06",
"sound.option.bipbop07": "بيب بوب 07",
"sound.option.bipbop08": "بيب بوب 08",
"sound.option.bipbop09": "بيب بوب 09",
"sound.option.bipbop10": "بيب بوب 10",
"sound.option.staplebops01": "ستابل بوبس 01",
"sound.option.staplebops02": "ستابل بوبس 02",
"sound.option.staplebops03": "ستابل بوبس 03",
"sound.option.staplebops04": "ستابل بوبس 04",
"sound.option.staplebops05": "ستابل بوبس 05",
"sound.option.staplebops06": "ستابل بوبس 06",
"sound.option.staplebops07": "ستابل بوبس 07",
"sound.option.nope01": "كلا 01",
"sound.option.nope02": "كلا 02",
"sound.option.nope03": "كلا 03",
"sound.option.nope04": "كلا 04",
"sound.option.nope05": "كلا 05",
"sound.option.nope06": "كلا 06",
"sound.option.nope07": "كلا 07",
"sound.option.nope08": "كلا 08",
"sound.option.nope09": "كلا 09",
"sound.option.nope10": "كلا 10",
"sound.option.nope11": "كلا 11",
"sound.option.nope12": "كلا 12",
"sound.option.yup01": "نعم 01",
"sound.option.yup02": "نعم 02",
"sound.option.yup03": "نعم 03",
"sound.option.yup04": "نعم 04",
"sound.option.yup05": "نعم 05",
"sound.option.yup06": "نعم 06",
"settings.general.notifications.agent.title": "وكيل",
"settings.general.notifications.agent.description": "عرض إشعار النظام عندما يكتمل الوكيل أو يحتاج إلى اهتمام",
"settings.general.notifications.permissions.title": "أذونات",
"settings.general.notifications.permissions.description": "عرض إشعار النظام عند الحاجة إلى إذن",
"settings.general.notifications.errors.title": "أخطاء",
"settings.general.notifications.errors.description": "عرض إشعار النظام عند حدوث خطأ",
"settings.general.sounds.agent.title": "وكيل",
"settings.general.sounds.agent.description": "تشغيل صوت عندما يكتمل الوكيل أو يحتاج إلى اهتمام",
"settings.general.sounds.permissions.title": "أذونات",
"settings.general.sounds.permissions.description": "تشغيل صوت عند الحاجة إلى إذن",
"settings.general.sounds.errors.title": "أخطاء",
"settings.general.sounds.errors.description": "تشغيل صوت عند حدوث خطأ",
"settings.shortcuts.title": "اختصارات لوحة المفاتيح",
"settings.shortcuts.reset.button": "إعادة التعيين إلى الافتراضيات",
"settings.shortcuts.reset.toast.title": "تم إعادة تعيين الاختصارات",
"settings.shortcuts.reset.toast.description": "تم إعادة تعيين اختصارات لوحة المفاتيح إلى الافتراضيات.",
"settings.shortcuts.conflict.title": "الاختصار قيد الاستخدام بالفعل",
"settings.shortcuts.conflict.description": "{{keybind}} معين بالفعل لـ {{titles}}.",
"settings.shortcuts.unassigned": "غير معين",
"settings.shortcuts.pressKeys": "اضغط على المفاتيح",
"settings.shortcuts.search.placeholder": "البحث في الاختصارات",
"settings.shortcuts.search.empty": "لم يتم العثور على اختصارات",
"settings.shortcuts.group.general": "عام",
"settings.shortcuts.group.session": "جلسة",
"settings.shortcuts.group.navigation": "تصفح",
"settings.shortcuts.group.modelAndAgent": "النموذج والوكيل",
"settings.shortcuts.group.terminal": "المحطة الطرفية",
"settings.shortcuts.group.prompt": "موجه",
"settings.providers.title": "الموفرون",
"settings.providers.description": "ستكون إعدادات الموفر قابلة للتكوين هنا.",
"settings.models.title": "النماذج",
"settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.",
"settings.agents.title": "الوكلاء",
"settings.agents.description": "ستكون إعدادات الوكيل قابلة للتكوين هنا.",
"settings.commands.title": "الأوامر",
"settings.commands.description": "ستكون إعدادات الأمر قابلة للتكوين هنا.",
"settings.mcp.title": "MCP",
"settings.mcp.description": "ستكون إعدادات MCP قابلة للتكوين هنا.",
"settings.permissions.title": "الأذونات",
"settings.permissions.description": "تحكم في الأدوات التي يمكن للخادم استخدامها بشكل افتراضي.",
"settings.permissions.section.tools": "الأدوات",
"settings.permissions.toast.updateFailed.title": "فشل تحديث الأذونات",
"settings.permissions.action.allow": "سماح",
"settings.permissions.action.ask": "سؤال",
"settings.permissions.action.deny": "رفض",
"settings.permissions.tool.read.title": "قراءة",
"settings.permissions.tool.read.description": "قراءة ملف (يطابق مسار الملف)",
"settings.permissions.tool.edit.title": "تحرير",
"settings.permissions.tool.edit.description":
"تعديل الملفات، بما في ذلك التحرير والكتابة والتصحيحات والتحرير المتعدد",
"settings.permissions.tool.glob.title": "Glob",
"settings.permissions.tool.glob.description": "مطابقة الملفات باستخدام أنماط glob",
"settings.permissions.tool.grep.title": "Grep",
"settings.permissions.tool.grep.description": "البحث في محتويات الملف باستخدام التعبيرات العادية",
"settings.permissions.tool.list.title": "قائمة",
"settings.permissions.tool.list.description": "سرد الملفات داخل دليل",
"settings.permissions.tool.bash.title": "Bash",
"settings.permissions.tool.bash.description": "تشغيل أوامر shell",
"settings.permissions.tool.task.title": "Task",
"settings.permissions.tool.task.description": "تشغيل الوكلاء الفرعيين",
"settings.permissions.tool.skill.title": "Skill",
"settings.permissions.tool.skill.description": "تحميل مهارة بالاسم",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "تشغيل استعلامات خادم اللغة",
"settings.permissions.tool.todoread.title": "قراءة المهام",
"settings.permissions.tool.todoread.description": "قراءة قائمة المهام",
"settings.permissions.tool.todowrite.title": "كتابة المهام",
"settings.permissions.tool.todowrite.description": "تحديث قائمة المهام",
"settings.permissions.tool.webfetch.title": "جلب الويب",
"settings.permissions.tool.webfetch.description": "جلب محتوى من عنوان URL",
"settings.permissions.tool.websearch.title": "بحث الويب",
"settings.permissions.tool.websearch.description": "البحث في الويب",
"settings.permissions.tool.codesearch.title": "بحث الكود",
"settings.permissions.tool.codesearch.description": "البحث عن كود على الويب",
"settings.permissions.tool.external_directory.title": "دليل خارجي",
"settings.permissions.tool.external_directory.description": "الوصول إلى الملفات خارج دليل المشروع",
"settings.permissions.tool.doom_loop.title": "حلقة الموت",
"settings.permissions.tool.doom_loop.description": "اكتشاف استدعاءات الأدوات المتكررة بمدخلات متطابقة",
"session.delete.failed.title": "فشل حذف الجلسة",
"session.delete.title": "حذف الجلسة",
"session.delete.confirm": 'حذف الجلسة "{{name}}"؟',
"session.delete.button": "حذف الجلسة",
"workspace.new": "مساحة عمل جديدة",
"workspace.type.local": "محلي",
"workspace.type.sandbox": "صندوق رمل",
"workspace.create.failed.title": "فشل إنشاء مساحة العمل",
"workspace.delete.failed.title": "فشل حذف مساحة العمل",
"workspace.resetting.title": "إعادة تعيين مساحة العمل",
"workspace.resetting.description": "قد يستغرق هذا دقيقة.",
"workspace.reset.failed.title": "فشل إعادة تعيين مساحة العمل",
"workspace.reset.success.title": "تمت إعادة تعيين مساحة العمل",
"workspace.reset.success.description": "مساحة العمل تطابق الآن الفرع الافتراضي.",
"workspace.status.checking": "التحقق من التغييرات غير المدمجة...",
"workspace.status.error": "تعذر التحقق من حالة git.",
"workspace.status.clean": "لم يتم اكتشاف تغييرات غير مدمجة.",
"workspace.status.dirty": "تم اكتشاف تغييرات غير مدمجة في مساحة العمل هذه.",
"workspace.delete.title": "حذف مساحة العمل",
"workspace.delete.confirm": 'حذف مساحة العمل "{{name}}"؟',
"workspace.delete.button": "حذف مساحة العمل",
"workspace.reset.title": "إعادة تعيين مساحة العمل",
"workspace.reset.confirm": 'إعادة تعيين مساحة العمل "{{name}}"؟',
"workspace.reset.button": "إعادة تعيين مساحة العمل",
"workspace.reset.archived.none": "لن تتم أرشفة أي جلسات نشطة.",
"workspace.reset.archived.one": "ستتم أرشفة جلسة واحدة.",
"workspace.reset.archived.many": "ستتم أرشفة {{count}} جلسات.",
"workspace.reset.note": "سيؤدي هذا إلى إعادة تعيين مساحة العمل لتتطابق مع الفرع الافتراضي.",
}

675
packages/app/src/i18n/br.ts Normal file
View File

@@ -0,0 +1,675 @@
export const dict = {
"command.category.suggested": "Sugerido",
"command.category.view": "Visualizar",
"command.category.project": "Projeto",
"command.category.provider": "Provedor",
"command.category.server": "Servidor",
"command.category.session": "Sessão",
"command.category.theme": "Tema",
"command.category.language": "Idioma",
"command.category.file": "Arquivo",
"command.category.terminal": "Terminal",
"command.category.model": "Modelo",
"command.category.mcp": "MCP",
"command.category.agent": "Agente",
"command.category.permissions": "Permissões",
"command.category.workspace": "Espaço de trabalho",
"command.category.settings": "Configurações",
"theme.scheme.system": "Sistema",
"theme.scheme.light": "Claro",
"theme.scheme.dark": "Escuro",
"command.sidebar.toggle": "Alternar barra lateral",
"command.project.open": "Abrir projeto",
"command.provider.connect": "Conectar provedor",
"command.server.switch": "Trocar servidor",
"command.settings.open": "Abrir configurações",
"command.session.previous": "Sessão anterior",
"command.session.next": "Próxima sessão",
"command.session.archive": "Arquivar sessão",
"command.palette": "Paleta de comandos",
"command.theme.cycle": "Alternar tema",
"command.theme.set": "Usar tema: {{theme}}",
"command.theme.scheme.cycle": "Alternar esquema de cores",
"command.theme.scheme.set": "Usar esquema de cores: {{scheme}}",
"command.language.cycle": "Alternar idioma",
"command.language.set": "Usar idioma: {{language}}",
"command.session.new": "Nova sessão",
"command.file.open": "Abrir arquivo",
"command.file.open.description": "Buscar arquivos e comandos",
"command.terminal.toggle": "Alternar terminal",
"command.review.toggle": "Alternar revisão",
"command.terminal.new": "Novo terminal",
"command.terminal.new.description": "Criar uma nova aba de terminal",
"command.steps.toggle": "Alternar passos",
"command.steps.toggle.description": "Mostrar ou ocultar passos da mensagem atual",
"command.message.previous": "Mensagem anterior",
"command.message.previous.description": "Ir para a mensagem de usuário anterior",
"command.message.next": "Próxima mensagem",
"command.message.next.description": "Ir para a próxima mensagem de usuário",
"command.model.choose": "Escolher modelo",
"command.model.choose.description": "Selecionar um modelo diferente",
"command.mcp.toggle": "Alternar MCPs",
"command.mcp.toggle.description": "Alternar MCPs",
"command.agent.cycle": "Alternar agente",
"command.agent.cycle.description": "Mudar para o próximo agente",
"command.agent.cycle.reverse": "Alternar agente (reverso)",
"command.agent.cycle.reverse.description": "Mudar para o agente anterior",
"command.model.variant.cycle": "Alternar nível de raciocínio",
"command.model.variant.cycle.description": "Mudar para o próximo nível de esforço",
"command.permissions.autoaccept.enable": "Aceitar edições automaticamente",
"command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente",
"command.session.undo": "Desfazer",
"command.session.undo.description": "Desfazer a última mensagem",
"command.session.redo": "Refazer",
"command.session.redo.description": "Refazer a última mensagem desfeita",
"command.session.compact": "Compactar sessão",
"command.session.compact.description": "Resumir a sessão para reduzir o tamanho do contexto",
"command.session.fork": "Bifurcar da mensagem",
"command.session.fork.description": "Criar uma nova sessão a partir de uma mensagem anterior",
"command.session.share": "Compartilhar sessão",
"command.session.share.description": "Compartilhar esta sessão e copiar a URL para a área de transferência",
"command.session.unshare": "Parar de compartilhar sessão",
"command.session.unshare.description": "Parar de compartilhar esta sessão",
"palette.search.placeholder": "Buscar arquivos e comandos",
"palette.empty": "Nenhum resultado encontrado",
"palette.group.commands": "Comandos",
"palette.group.files": "Arquivos",
"dialog.provider.search.placeholder": "Buscar provedores",
"dialog.provider.empty": "Nenhum provedor encontrado",
"dialog.provider.group.popular": "Popular",
"dialog.provider.group.other": "Outro",
"dialog.provider.tag.recommended": "Recomendado",
"dialog.provider.anthropic.note": "Conectar com Claude Pro/Max ou chave de API",
"dialog.provider.openai.note": "Conectar com ChatGPT Pro/Plus ou chave de API",
"dialog.provider.copilot.note": "Conectar com Copilot ou chave de API",
"dialog.model.select.title": "Selecionar modelo",
"dialog.model.search.placeholder": "Buscar modelos",
"dialog.model.empty": "Nenhum resultado de modelo",
"dialog.model.manage": "Gerenciar modelos",
"dialog.model.manage.description": "Personalizar quais modelos aparecem no seletor de modelos.",
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo OpenCode",
"dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares",
"dialog.provider.viewAll": "Ver mais provedores",
"provider.connect.title": "Conectar {{provider}}",
"provider.connect.title.anthropicProMax": "Entrar com Claude Pro/Max",
"provider.connect.selectMethod": "Selecionar método de login para {{provider}}.",
"provider.connect.method.apiKey": "Chave de API",
"provider.connect.status.inProgress": "Autorização em andamento...",
"provider.connect.status.waiting": "Aguardando autorização...",
"provider.connect.status.failed": "Autorização falhou: {{error}}",
"provider.connect.apiKey.description":
"Digite sua chave de API do {{provider}} para conectar sua conta e usar modelos do {{provider}} no OpenCode.",
"provider.connect.apiKey.label": "Chave de API do {{provider}}",
"provider.connect.apiKey.placeholder": "Chave de API",
"provider.connect.apiKey.required": "A chave de API é obrigatória",
"provider.connect.opencodeZen.line1":
"OpenCode Zen oferece acesso a um conjunto selecionado de modelos confiáveis otimizados para agentes de código.",
"provider.connect.opencodeZen.line2":
"Com uma única chave de API você terá acesso a modelos como Claude, GPT, Gemini, GLM e mais.",
"provider.connect.opencodeZen.visit.prefix": "Visite ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": " para obter sua chave de API.",
"provider.connect.oauth.code.visit.prefix": "Visite ",
"provider.connect.oauth.code.visit.link": "este link",
"provider.connect.oauth.code.visit.suffix":
" para obter seu código de autorização e conectar sua conta para usar modelos do {{provider}} no OpenCode.",
"provider.connect.oauth.code.label": "Código de autorização {{method}}",
"provider.connect.oauth.code.placeholder": "Código de autorização",
"provider.connect.oauth.code.required": "O código de autorização é obrigatório",
"provider.connect.oauth.code.invalid": "Código de autorização inválido",
"provider.connect.oauth.auto.visit.prefix": "Visite ",
"provider.connect.oauth.auto.visit.link": "este link",
"provider.connect.oauth.auto.visit.suffix":
" e digite o código abaixo para conectar sua conta e usar modelos do {{provider}} no OpenCode.",
"provider.connect.oauth.auto.confirmationCode": "Código de confirmação",
"provider.connect.toast.connected.title": "{{provider}} conectado",
"provider.connect.toast.connected.description": "Modelos do {{provider}} agora estão disponíveis para uso.",
"model.tag.free": "Grátis",
"model.tag.latest": "Mais recente",
"model.provider.anthropic": "Anthropic",
"model.provider.openai": "OpenAI",
"model.provider.google": "Google",
"model.provider.xai": "xAI",
"model.provider.meta": "Meta",
"model.input.text": "texto",
"model.input.image": "imagem",
"model.input.audio": "áudio",
"model.input.video": "vídeo",
"model.input.pdf": "pdf",
"model.tooltip.allows": "Permite: {{inputs}}",
"model.tooltip.reasoning.allowed": "Permite raciocínio",
"model.tooltip.reasoning.none": "Sem raciocínio",
"model.tooltip.context": "Limite de contexto {{limit}}",
"common.search.placeholder": "Buscar",
"common.goBack": "Voltar",
"common.loading": "Carregando",
"common.loading.ellipsis": "...",
"common.cancel": "Cancelar",
"common.submit": "Enviar",
"common.save": "Salvar",
"common.saving": "Salvando...",
"common.default": "Padrão",
"common.attachment": "anexo",
"prompt.placeholder.shell": "Digite comando do shell...",
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc para sair",
"prompt.example.1": "Corrigir um TODO no código",
"prompt.example.2": "Qual é a stack tecnológica deste projeto?",
"prompt.example.3": "Corrigir testes quebrados",
"prompt.example.4": "Explicar como funciona a autenticação",
"prompt.example.5": "Encontrar e corrigir vulnerabilidades de segurança",
"prompt.example.6": "Adicionar testes unitários para o serviço de usuário",
"prompt.example.7": "Refatorar esta função para melhor legibilidade",
"prompt.example.8": "O que significa este erro?",
"prompt.example.9": "Me ajude a depurar este problema",
"prompt.example.10": "Gerar documentação da API",
"prompt.example.11": "Otimizar consultas ao banco de dados",
"prompt.example.12": "Adicionar validação de entrada",
"prompt.example.13": "Criar um novo componente para...",
"prompt.example.14": "Como faço o deploy deste projeto?",
"prompt.example.15": "Revisar meu código para boas práticas",
"prompt.example.16": "Adicionar tratamento de erros a esta função",
"prompt.example.17": "Explicar este padrão regex",
"prompt.example.18": "Converter isto para TypeScript",
"prompt.example.19": "Adicionar logging em todo o código",
"prompt.example.20": "Quais dependências estão desatualizadas?",
"prompt.example.21": "Me ajude a escrever um script de migração",
"prompt.example.22": "Implementar cache para este endpoint",
"prompt.example.23": "Adicionar paginação a esta lista",
"prompt.example.24": "Criar um comando CLI para...",
"prompt.example.25": "Como funcionam as variáveis de ambiente aqui?",
"prompt.popover.emptyResults": "Nenhum resultado correspondente",
"prompt.popover.emptyCommands": "Nenhum comando correspondente",
"prompt.dropzone.label": "Solte imagens ou PDFs aqui",
"prompt.slash.badge.custom": "personalizado",
"prompt.context.active": "ativo",
"prompt.context.includeActiveFile": "Incluir arquivo ativo",
"prompt.context.removeActiveFile": "Remover arquivo ativo do contexto",
"prompt.context.removeFile": "Remover arquivo do contexto",
"prompt.action.attachFile": "Anexar arquivo",
"prompt.attachment.remove": "Remover anexo",
"prompt.action.send": "Enviar",
"prompt.action.stop": "Parar",
"prompt.toast.pasteUnsupported.title": "Colagem não suportada",
"prompt.toast.pasteUnsupported.description": "Somente imagens ou PDFs podem ser colados aqui.",
"prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo",
"prompt.toast.modelAgentRequired.description": "Escolha um agente e modelo antes de enviar um prompt.",
"prompt.toast.worktreeCreateFailed.title": "Falha ao criar worktree",
"prompt.toast.sessionCreateFailed.title": "Falha ao criar sessão",
"prompt.toast.shellSendFailed.title": "Falha ao enviar comando shell",
"prompt.toast.commandSendFailed.title": "Falha ao enviar comando",
"prompt.toast.promptSendFailed.title": "Falha ao enviar prompt",
"dialog.mcp.title": "MCPs",
"dialog.mcp.description": "{{enabled}} de {{total}} habilitados",
"dialog.mcp.empty": "Nenhum MCP configurado",
"mcp.status.connected": "conectado",
"mcp.status.failed": "falhou",
"mcp.status.needs_auth": "precisa de autenticação",
"mcp.status.disabled": "desabilitado",
"dialog.fork.empty": "Nenhuma mensagem para bifurcar",
"dialog.directory.search.placeholder": "Buscar pastas",
"dialog.directory.empty": "Nenhuma pasta encontrada",
"dialog.server.title": "Servidores",
"dialog.server.description": "Trocar para qual servidor OpenCode este aplicativo se conecta.",
"dialog.server.search.placeholder": "Buscar servidores",
"dialog.server.empty": "Nenhum servidor ainda",
"dialog.server.add.title": "Adicionar um servidor",
"dialog.server.add.url": "URL do servidor",
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "Não foi possível conectar ao servidor",
"dialog.server.add.checking": "Verificando...",
"dialog.server.add.button": "Adicionar",
"dialog.server.default.title": "Servidor padrão",
"dialog.server.default.description":
"Conectar a este servidor na inicialização do aplicativo ao invés de iniciar um servidor local. Requer reinicialização.",
"dialog.server.default.none": "Nenhum servidor selecionado",
"dialog.server.default.set": "Definir servidor atual como padrão",
"dialog.server.default.clear": "Limpar",
"dialog.server.action.remove": "Remover servidor",
"dialog.project.edit.title": "Editar projeto",
"dialog.project.edit.name": "Nome",
"dialog.project.edit.icon": "Ícone",
"dialog.project.edit.icon.alt": "Ícone do projeto",
"dialog.project.edit.icon.hint": "Clique ou arraste uma imagem",
"dialog.project.edit.icon.recommended": "Recomendado: 128x128px",
"dialog.project.edit.color": "Cor",
"dialog.project.edit.color.select": "Selecionar cor {{color}}",
"dialog.project.edit.worktree.startup": "Script de inicialização do espaço de trabalho",
"dialog.project.edit.worktree.startup.description": "Executa após criar um novo espaço de trabalho (worktree).",
"dialog.project.edit.worktree.startup.placeholder": "ex: bun install",
"context.breakdown.title": "Detalhamento do Contexto",
"context.breakdown.note":
'Detalhamento aproximado dos tokens de entrada. "Outros" inclui definições de ferramentas e overhead.',
"context.breakdown.system": "Sistema",
"context.breakdown.user": "Usuário",
"context.breakdown.assistant": "Assistente",
"context.breakdown.tool": "Chamadas de Ferramentas",
"context.breakdown.other": "Outros",
"context.systemPrompt.title": "Prompt do Sistema",
"context.rawMessages.title": "Mensagens brutas",
"context.stats.session": "Sessão",
"context.stats.messages": "Mensagens",
"context.stats.provider": "Provedor",
"context.stats.model": "Modelo",
"context.stats.limit": "Limite de Contexto",
"context.stats.totalTokens": "Total de Tokens",
"context.stats.usage": "Uso",
"context.stats.inputTokens": "Tokens de Entrada",
"context.stats.outputTokens": "Tokens de Saída",
"context.stats.reasoningTokens": "Tokens de Raciocínio",
"context.stats.cacheTokens": "Tokens de Cache (leitura/escrita)",
"context.stats.userMessages": "Mensagens de Usuário",
"context.stats.assistantMessages": "Mensagens do Assistente",
"context.stats.totalCost": "Custo Total",
"context.stats.sessionCreated": "Sessão Criada",
"context.stats.lastActivity": "Última Atividade",
"context.usage.tokens": "Tokens",
"context.usage.usage": "Uso",
"context.usage.cost": "Custo",
"context.usage.clickToView": "Clique para ver o contexto",
"context.usage.view": "Ver uso do contexto",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"toast.language.title": "Idioma",
"toast.language.description": "Alterado para {{language}}",
"toast.theme.title": "Tema alterado",
"toast.scheme.title": "Esquema de cores",
"toast.permissions.autoaccept.on.title": "Aceitando edições automaticamente",
"toast.permissions.autoaccept.on.description": "Permissões de edição e escrita serão aprovadas 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.model.none.title": "Nenhum modelo selecionado",
"toast.model.none.description": "Conecte um provedor para resumir esta sessão",
"toast.file.loadFailed.title": "Falha ao carregar arquivo",
"toast.session.share.copyFailed.title": "Falha ao copiar URL para a área de transferência",
"toast.session.share.success.title": "Sessão compartilhada",
"toast.session.share.success.description": "URL compartilhada copiada para a área de transferência!",
"toast.session.share.failed.title": "Falha ao compartilhar sessão",
"toast.session.share.failed.description": "Ocorreu um erro ao compartilhar a sessão",
"toast.session.unshare.success.title": "Sessão não compartilhada",
"toast.session.unshare.success.description": "Sessão deixou de ser compartilhada com sucesso!",
"toast.session.unshare.failed.title": "Falha ao parar de compartilhar sessão",
"toast.session.unshare.failed.description": "Ocorreu um erro ao parar de compartilhar a sessão",
"toast.session.listFailed.title": "Falha ao carregar sessões para {{project}}",
"toast.update.title": "Atualização disponível",
"toast.update.description": "Uma nova versão do OpenCode ({{version}}) está disponível para instalação.",
"toast.update.action.installRestart": "Instalar e reiniciar",
"toast.update.action.notYet": "Agora não",
"error.page.title": "Algo deu errado",
"error.page.description": "Ocorreu um erro ao carregar a aplicação.",
"error.page.details.label": "Detalhes do Erro",
"error.page.action.restart": "Reiniciar",
"error.page.action.checking": "Verificando...",
"error.page.action.checkUpdates": "Verificar atualizações",
"error.page.action.updateTo": "Atualizar para {{version}}",
"error.page.report.prefix": "Por favor, reporte este erro para a equipe do OpenCode",
"error.page.report.discord": "no Discord",
"error.page.version": "Versão: {{version}}",
"error.dev.rootNotFound":
"Elemento raiz não encontrado. Você esqueceu de adicioná-lo ao seu index.html? Ou talvez o atributo id foi escrito incorretamente?",
"error.globalSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?",
"error.chain.unknown": "Erro desconhecido",
"error.chain.causedBy": "Causado por:",
"error.chain.apiError": "Erro de API",
"error.chain.status": "Status: {{status}}",
"error.chain.retryable": "Pode tentar novamente: {{retryable}}",
"error.chain.responseBody": "Corpo da resposta:\n{{body}}",
"error.chain.didYouMean": "Você quis dizer: {{suggestions}}",
"error.chain.modelNotFound": "Modelo não encontrado: {{provider}}/{{model}}",
"error.chain.checkConfig": "Verifique os nomes de provedor/modelo na sua configuração (opencode.json)",
"error.chain.mcpFailed": 'Servidor MCP "{{name}}" falhou. Nota: OpenCode ainda não suporta autenticação MCP.',
"error.chain.providerAuthFailed": "Autenticação do provedor falhou ({{provider}}): {{message}}",
"error.chain.providerInitFailed":
'Falha ao inicializar provedor "{{provider}}". Verifique credenciais e configuração.',
"error.chain.configJsonInvalid": "Arquivo de configuração em {{path}} não é um JSON(C) válido",
"error.chain.configJsonInvalidWithMessage":
"Arquivo de configuração em {{path}} não é um JSON(C) válido: {{message}}",
"error.chain.configDirectoryTypo":
'Diretório "{{dir}}" em {{path}} não é válido. Renomeie o diretório para "{{suggestion}}" ou remova-o. Este é um erro de digitação comum.',
"error.chain.configFrontmatterError": "Falha ao analisar frontmatter em {{path}}:\n{{message}}",
"error.chain.configInvalid": "Arquivo de configuração em {{path}} é inválido",
"error.chain.configInvalidWithMessage": "Arquivo de configuração em {{path}} é inválido: {{message}}",
"notification.permission.title": "Permissão necessária",
"notification.permission.description": "{{sessionTitle}} em {{projectName}} precisa de permissão",
"notification.question.title": "Pergunta",
"notification.question.description": "{{sessionTitle}} em {{projectName}} tem uma pergunta",
"notification.action.goToSession": "Ir para sessão",
"notification.session.responseReady.title": "Resposta pronta",
"notification.session.error.title": "Erro na sessão",
"notification.session.error.fallbackDescription": "Ocorreu um erro",
"home.recentProjects": "Projetos recentes",
"home.empty.title": "Nenhum projeto recente",
"home.empty.description": "Comece abrindo um projeto local",
"session.tab.session": "Sessão",
"session.tab.review": "Revisão",
"session.tab.context": "Contexto",
"session.panel.reviewAndFiles": "Revisão e arquivos",
"session.review.filesChanged": "{{count}} Arquivos Alterados",
"session.review.loadingChanges": "Carregando alterações...",
"session.review.empty": "Nenhuma alteração nesta sessão ainda",
"session.messages.renderEarlier": "Renderizar mensagens anteriores",
"session.messages.loadingEarlier": "Carregando mensagens anteriores...",
"session.messages.loadEarlier": "Carregar mensagens anteriores",
"session.messages.loading": "Carregando mensagens...",
"session.messages.jumpToLatest": "Ir para a mais recente",
"session.context.addToContext": "Adicionar {{selection}} ao contexto",
"session.new.worktree.main": "Branch principal",
"session.new.worktree.mainWithBranch": "Branch principal ({{branch}})",
"session.new.worktree.create": "Criar novo worktree",
"session.new.lastModified": "Última modificação",
"session.header.search.placeholder": "Buscar {{project}}",
"session.header.searchFiles": "Buscar arquivos",
"status.popover.trigger": "Status",
"status.popover.ariaLabel": "Configurações de servidores",
"status.popover.tab.servers": "Servidores",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Gerenciar servidores",
"session.share.popover.title": "Publicar na web",
"session.share.popover.description.shared":
"Esta sessão é pública na web. Está acessível para qualquer pessoa com o link.",
"session.share.popover.description.unshared":
"Compartilhar sessão publicamente na web. Estará acessível para qualquer pessoa com o link.",
"session.share.action.share": "Compartilhar",
"session.share.action.publish": "Publicar",
"session.share.action.publishing": "Publicando...",
"session.share.action.unpublish": "Cancelar publicação",
"session.share.action.unpublishing": "Cancelando publicação...",
"session.share.action.view": "Ver",
"session.share.copy.copied": "Copiado",
"session.share.copy.copyLink": "Copiar link",
"lsp.tooltip.none": "Nenhum servidor LSP",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Carregando prompt...",
"terminal.loading": "Carregando terminal...",
"terminal.title": "Terminal",
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Fechar terminal",
"terminal.connectionLost.title": "Conexão Perdida",
"terminal.connectionLost.description":
"A conexão do terminal foi interrompida. Isso pode acontecer quando o servidor reinicia.",
"common.closeTab": "Fechar aba",
"common.dismiss": "Descartar",
"common.requestFailed": "Requisição falhou",
"common.moreOptions": "Mais opções",
"common.learnMore": "Saiba mais",
"common.rename": "Renomear",
"common.reset": "Redefinir",
"common.archive": "Arquivar",
"common.delete": "Excluir",
"common.close": "Fechar",
"common.edit": "Editar",
"common.loadMore": "Carregar mais",
"common.key.esc": "ESC",
"sidebar.menu.toggle": "Alternar menu",
"sidebar.nav.projectsAndSessions": "Projetos e sessões",
"sidebar.settings": "Configurações",
"sidebar.help": "Ajuda",
"sidebar.workspaces.enable": "Habilitar espaços de trabalho",
"sidebar.workspaces.disable": "Desabilitar espaços de trabalho",
"sidebar.gettingStarted.title": "Começando",
"sidebar.gettingStarted.line1": "OpenCode inclui modelos gratuitos para você começar imediatamente.",
"sidebar.gettingStarted.line2": "Conecte qualquer provedor para usar modelos, incluindo Claude, GPT, Gemini etc.",
"sidebar.project.recentSessions": "Sessões recentes",
"sidebar.project.viewAllSessions": "Ver todas as sessões",
"settings.section.desktop": "Desktop",
"settings.tab.general": "Geral",
"settings.tab.shortcuts": "Atalhos",
"settings.general.section.appearance": "Aparência",
"settings.general.section.notifications": "Notificações do sistema",
"settings.general.section.sounds": "Efeitos sonoros",
"settings.general.row.language.title": "Idioma",
"settings.general.row.language.description": "Alterar o idioma de exibição do OpenCode",
"settings.general.row.appearance.title": "Aparência",
"settings.general.row.appearance.description": "Personalize como o OpenCode aparece no seu dispositivo",
"settings.general.row.theme.title": "Tema",
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
"settings.general.row.font.title": "Fonte",
"settings.general.row.font.description": "Personalize a fonte monoespaçada usada em blocos de código",
"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.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": "Alerta 01",
"sound.option.alert02": "Alerta 02",
"sound.option.alert03": "Alerta 03",
"sound.option.alert04": "Alerta 04",
"sound.option.alert05": "Alerta 05",
"sound.option.alert06": "Alerta 06",
"sound.option.alert07": "Alerta 07",
"sound.option.alert08": "Alerta 08",
"sound.option.alert09": "Alerta 09",
"sound.option.alert10": "Alerta 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": "Não 01",
"sound.option.nope02": "Não 02",
"sound.option.nope03": "Não 03",
"sound.option.nope04": "Não 04",
"sound.option.nope05": "Não 05",
"sound.option.nope06": "Não 06",
"sound.option.nope07": "Não 07",
"sound.option.nope08": "Não 08",
"sound.option.nope09": "Não 09",
"sound.option.nope10": "Não 10",
"sound.option.nope11": "Não 11",
"sound.option.nope12": "Não 12",
"sound.option.yup01": "Sim 01",
"sound.option.yup02": "Sim 02",
"sound.option.yup03": "Sim 03",
"sound.option.yup04": "Sim 04",
"sound.option.yup05": "Sim 05",
"sound.option.yup06": "Sim 06",
"settings.general.notifications.agent.title": "Agente",
"settings.general.notifications.agent.description":
"Mostrar notificação do sistema quando o agente estiver completo ou precisar de atenção",
"settings.general.notifications.permissions.title": "Permissões",
"settings.general.notifications.permissions.description":
"Mostrar notificação do sistema quando uma permissão for necessária",
"settings.general.notifications.errors.title": "Erros",
"settings.general.notifications.errors.description": "Mostrar notificação do sistema quando ocorrer um erro",
"settings.general.sounds.agent.title": "Agente",
"settings.general.sounds.agent.description": "Reproduzir som quando o agente estiver completo ou precisar de atenção",
"settings.general.sounds.permissions.title": "Permissões",
"settings.general.sounds.permissions.description": "Reproduzir som quando uma permissão for necessária",
"settings.general.sounds.errors.title": "Erros",
"settings.general.sounds.errors.description": "Reproduzir som quando ocorrer um erro",
"settings.shortcuts.title": "Atalhos de teclado",
"settings.shortcuts.reset.button": "Redefinir para padrões",
"settings.shortcuts.reset.toast.title": "Atalhos redefinidos",
"settings.shortcuts.reset.toast.description": "Atalhos de teclado foram redefinidos para os padrões.",
"settings.shortcuts.conflict.title": "Atalho já em uso",
"settings.shortcuts.conflict.description": "{{keybind}} já está atribuído a {{titles}}.",
"settings.shortcuts.unassigned": "Não atribuído",
"settings.shortcuts.pressKeys": "Pressione teclas",
"settings.shortcuts.search.placeholder": "Buscar atalhos",
"settings.shortcuts.search.empty": "Nenhum atalho encontrado",
"settings.shortcuts.group.general": "Geral",
"settings.shortcuts.group.session": "Sessão",
"settings.shortcuts.group.navigation": "Navegação",
"settings.shortcuts.group.modelAndAgent": "Modelo e agente",
"settings.shortcuts.group.terminal": "Terminal",
"settings.shortcuts.group.prompt": "Prompt",
"settings.providers.title": "Provedores",
"settings.providers.description": "Configurações de provedores estarão disponíveis aqui.",
"settings.models.title": "Modelos",
"settings.models.description": "Configurações de modelos estarão disponíveis aqui.",
"settings.agents.title": "Agentes",
"settings.agents.description": "Configurações de agentes estarão disponíveis aqui.",
"settings.commands.title": "Comandos",
"settings.commands.description": "Configurações de comandos estarão disponíveis aqui.",
"settings.mcp.title": "MCP",
"settings.mcp.description": "Configurações de MCP estarão disponíveis aqui.",
"settings.permissions.title": "Permissões",
"settings.permissions.description": "Controle quais ferramentas o servidor pode usar por padrão.",
"settings.permissions.section.tools": "Ferramentas",
"settings.permissions.toast.updateFailed.title": "Falha ao atualizar permissões",
"settings.permissions.action.allow": "Permitir",
"settings.permissions.action.ask": "Perguntar",
"settings.permissions.action.deny": "Negar",
"settings.permissions.tool.read.title": "Ler",
"settings.permissions.tool.read.description": "Ler um arquivo (corresponde ao caminho do arquivo)",
"settings.permissions.tool.edit.title": "Editar",
"settings.permissions.tool.edit.description":
"Modificar arquivos, incluindo edições, escritas, patches e multi-edições",
"settings.permissions.tool.glob.title": "Glob",
"settings.permissions.tool.glob.description": "Corresponder arquivos usando padrões glob",
"settings.permissions.tool.grep.title": "Grep",
"settings.permissions.tool.grep.description": "Buscar conteúdo de arquivos usando expressões regulares",
"settings.permissions.tool.list.title": "Listar",
"settings.permissions.tool.list.description": "Listar arquivos dentro de um diretório",
"settings.permissions.tool.bash.title": "Bash",
"settings.permissions.tool.bash.description": "Executar comandos shell",
"settings.permissions.tool.task.title": "Tarefa",
"settings.permissions.tool.task.description": "Lançar sub-agentes",
"settings.permissions.tool.skill.title": "Habilidade",
"settings.permissions.tool.skill.description": "Carregar uma habilidade por nome",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Executar consultas de servidor de linguagem",
"settings.permissions.tool.todoread.title": "Ler Tarefas",
"settings.permissions.tool.todoread.description": "Ler a lista de tarefas",
"settings.permissions.tool.todowrite.title": "Escrever Tarefas",
"settings.permissions.tool.todowrite.description": "Atualizar a lista de tarefas",
"settings.permissions.tool.webfetch.title": "Buscar Web",
"settings.permissions.tool.webfetch.description": "Buscar conteúdo de uma URL",
"settings.permissions.tool.websearch.title": "Pesquisa Web",
"settings.permissions.tool.websearch.description": "Pesquisar na web",
"settings.permissions.tool.codesearch.title": "Pesquisa de Código",
"settings.permissions.tool.codesearch.description": "Pesquisar código na web",
"settings.permissions.tool.external_directory.title": "Diretório Externo",
"settings.permissions.tool.external_directory.description": "Acessar arquivos fora do diretório do projeto",
"settings.permissions.tool.doom_loop.title": "Loop Infinito",
"settings.permissions.tool.doom_loop.description": "Detectar chamadas de ferramentas repetidas com entrada idêntica",
"session.delete.failed.title": "Falha ao excluir sessão",
"session.delete.title": "Excluir sessão",
"session.delete.confirm": 'Excluir sessão "{{name}}"?',
"session.delete.button": "Excluir sessão",
"workspace.new": "Novo espaço de trabalho",
"workspace.type.local": "local",
"workspace.type.sandbox": "sandbox",
"workspace.create.failed.title": "Falha ao criar espaço de trabalho",
"workspace.delete.failed.title": "Falha ao excluir espaço de trabalho",
"workspace.resetting.title": "Redefinindo espaço de trabalho",
"workspace.resetting.description": "Isso pode levar um minuto.",
"workspace.reset.failed.title": "Falha ao redefinir espaço de trabalho",
"workspace.reset.success.title": "Espaço de trabalho redefinido",
"workspace.reset.success.description": "Espaço de trabalho agora corresponde ao branch padrão.",
"workspace.status.checking": "Verificando alterações não mescladas...",
"workspace.status.error": "Não foi possível verificar o status do git.",
"workspace.status.clean": "Nenhuma alteração não mesclada detectada.",
"workspace.status.dirty": "Alterações não mescladas detectadas neste espaço de trabalho.",
"workspace.delete.title": "Excluir espaço de trabalho",
"workspace.delete.confirm": 'Excluir espaço de trabalho "{{name}}"?',
"workspace.delete.button": "Excluir espaço de trabalho",
"workspace.reset.title": "Redefinir espaço de trabalho",
"workspace.reset.confirm": 'Redefinir espaço de trabalho "{{name}}"?',
"workspace.reset.button": "Redefinir espaço de trabalho",
"workspace.reset.archived.none": "Nenhuma sessão ativa será arquivada.",
"workspace.reset.archived.one": "1 sessão será arquivada.",
"workspace.reset.archived.many": "{{count}} sessões serão arquivadas.",
"workspace.reset.note": "Isso redefinirá o espaço de trabalho para corresponder ao branch padrão.",
}

View File

@@ -86,6 +86,8 @@ export const dict = {
"dialog.provider.group.other": "Andre",
"dialog.provider.tag.recommended": "Anbefalet",
"dialog.provider.anthropic.note": "Forbind med Claude Pro/Max eller API-nøgle",
"dialog.provider.openai.note": "Forbind med ChatGPT Pro/Plus eller API-nøgle",
"dialog.provider.copilot.note": "Forbind med Copilot eller API-nøgle",
"dialog.model.select.title": "Vælg model",
"dialog.model.search.placeholder": "Søg modeller",
@@ -96,7 +98,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af OpenCode",
"dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere",
"dialog.provider.viewAll": "Vis alle udbydere",
"dialog.provider.viewAll": "Vis flere udbydere",
"provider.connect.title": "Forbind {{provider}}",
"provider.connect.title.anthropicProMax": "Log ind med Claude Pro/Max",
@@ -136,6 +138,7 @@ export const dict = {
"model.tag.latest": "Nyeste",
"common.search.placeholder": "Søg",
"common.goBack": "Gå tilbage",
"common.loading": "Indlæser",
"common.cancel": "Annuller",
"common.submit": "Indsend",
@@ -181,7 +184,10 @@ export const dict = {
"prompt.slash.badge.custom": "brugerdefineret",
"prompt.context.active": "aktiv",
"prompt.context.includeActiveFile": "Inkluder aktiv fil",
"prompt.context.removeActiveFile": "Fjern aktiv fil fra kontekst",
"prompt.context.removeFile": "Fjern fil fra kontekst",
"prompt.action.attachFile": "Vedhæft fil",
"prompt.attachment.remove": "Fjern vedhæftning",
"prompt.action.send": "Send",
"prompt.action.stop": "Stop",
@@ -199,6 +205,9 @@ export const dict = {
"dialog.mcp.description": "{{enabled}} af {{total}} aktiveret",
"dialog.mcp.empty": "Ingen MCP'er konfigureret",
"dialog.lsp.empty": "LSP'er registreret automatisk fra filtyper",
"dialog.plugins.empty": "Plugins konfigureret i opencode.json",
"mcp.status.connected": "forbundet",
"mcp.status.failed": "mislykkedes",
"mcp.status.needs_auth": "kræver godkendelse",
@@ -218,13 +227,21 @@ export const dict = {
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "Kunne ikke forbinde til server",
"dialog.server.add.checking": "Tjekker...",
"dialog.server.add.button": "Tilføj",
"dialog.server.add.button": "Tilføj server",
"dialog.server.default.title": "Standardserver",
"dialog.server.default.description":
"Forbind til denne server ved start af app i stedet for at starte en lokal server. Kræver genstart.",
"dialog.server.default.none": "Ingen server valgt",
"dialog.server.default.set": "Sæt nuværende server som standard",
"dialog.server.default.clear": "Ryd",
"dialog.server.action.remove": "Fjern server",
"dialog.server.menu.edit": "Rediger",
"dialog.server.menu.default": "Sæt som standard",
"dialog.server.menu.defaultRemove": "Fjern som standard",
"dialog.server.menu.delete": "Slet",
"dialog.server.current": "Nuværende server",
"dialog.server.status.default": "Standard",
"dialog.project.edit.title": "Rediger projekt",
"dialog.project.edit.name": "Navn",
@@ -233,6 +250,7 @@ export const dict = {
"dialog.project.edit.icon.hint": "Klik eller træk et billede",
"dialog.project.edit.icon.recommended": "Anbefalet: 128x128px",
"dialog.project.edit.color": "Farve",
"dialog.project.edit.color.select": "Vælg farven {{color}}",
"context.breakdown.title": "Kontekstfordeling",
"context.breakdown.note":
@@ -267,15 +285,22 @@ export const dict = {
"context.usage.usage": "Forbrug",
"context.usage.cost": "Omkostning",
"context.usage.clickToView": "Klik for at se kontekst",
"context.usage.view": "Se kontekstforbrug",
"language.en": "Engelsk",
"language.zh": "Kinesisk",
"language.ko": "Koreansk",
"language.de": "Tysk",
"language.es": "Spansk",
"language.fr": "Fransk",
"language.ja": "Japansk",
"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)",
"toast.language.title": "Sprog",
"toast.language.description": "Skiftede til {{language}}",
@@ -365,6 +390,7 @@ export const dict = {
"session.tab.session": "Session",
"session.tab.review": "Gennemgang",
"session.tab.context": "Kontekst",
"session.panel.reviewAndFiles": "Gennemgang og filer",
"session.review.filesChanged": "{{count}} Filer ændret",
"session.review.loadingChanges": "Indlæser ændringer...",
"session.review.empty": "Ingen ændringer i denne session endnu",
@@ -381,6 +407,15 @@ export const dict = {
"session.new.lastModified": "Sidst ændret",
"session.header.search.placeholder": "Søg {{project}}",
"session.header.searchFiles": "Søg efter filer",
"status.popover.trigger": "Status",
"status.popover.ariaLabel": "Serverkonfigurationer",
"status.popover.tab.servers": "Servere",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Administrer servere",
"session.share.popover.title": "Udgiv på nettet",
"session.share.popover.description.shared":
@@ -403,6 +438,7 @@ export const dict = {
"terminal.loading": "Indlæser terminal...",
"terminal.title": "Terminal",
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Luk terminal",
"common.closeTab": "Luk fane",
"common.dismiss": "Afvis",
@@ -411,11 +447,13 @@ export const dict = {
"common.learnMore": "Lær mere",
"common.rename": "Omdøb",
"common.reset": "Nulstil",
"common.archive": "Arkivér",
"common.delete": "Slet",
"common.close": "Luk",
"common.edit": "Rediger",
"common.loadMore": "Indlæs flere",
"sidebar.nav.projectsAndSessions": "Projekter og sessioner",
"sidebar.settings": "Indstillinger",
"sidebar.help": "Hjælp",
"sidebar.workspaces.enable": "Aktiver arbejdsområder",
@@ -530,6 +568,11 @@ export const dict = {
"settings.permissions.tool.doom_loop.title": "Doom Loop",
"settings.permissions.tool.doom_loop.description": "Opdag gentagne værktøjskald med identisk input",
"session.delete.failed.title": "Kunne ikke slette session",
"session.delete.title": "Slet session",
"session.delete.confirm": 'Slet session "{{name}}"?',
"session.delete.button": "Slet session",
"workspace.new": "Nyt arbejdsområde",
"workspace.type.local": "lokal",
"workspace.type.sandbox": "sandkasse",

View File

@@ -90,6 +90,8 @@ export const dict = {
"dialog.provider.group.other": "Andere",
"dialog.provider.tag.recommended": "Empfohlen",
"dialog.provider.anthropic.note": "Mit Claude Pro/Max oder API-Schlüssel verbinden",
"dialog.provider.openai.note": "Mit ChatGPT Pro/Plus oder API-Schlüssel verbinden",
"dialog.provider.copilot.note": "Mit Copilot oder API-Schlüssel verbinden",
"dialog.model.select.title": "Modell auswählen",
"dialog.model.search.placeholder": "Modelle durchsuchen",
@@ -100,7 +102,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode",
"dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen",
"dialog.provider.viewAll": "Alle Anbieter anzeigen",
"dialog.provider.viewAll": "Mehr Anbieter anzeigen",
"provider.connect.title": "{{provider}} verbinden",
"provider.connect.title.anthropicProMax": "Mit Claude Pro/Max anmelden",
@@ -140,6 +142,7 @@ export const dict = {
"model.tag.latest": "Neueste",
"common.search.placeholder": "Suchen",
"common.goBack": "Zurück",
"common.loading": "Laden",
"common.cancel": "Abbrechen",
"common.submit": "Absenden",
@@ -185,7 +188,10 @@ export const dict = {
"prompt.slash.badge.custom": "benutzerdefiniert",
"prompt.context.active": "aktiv",
"prompt.context.includeActiveFile": "Aktive Datei einbeziehen",
"prompt.context.removeActiveFile": "Aktive Datei aus dem Kontext entfernen",
"prompt.context.removeFile": "Datei aus dem Kontext entfernen",
"prompt.action.attachFile": "Datei anhängen",
"prompt.attachment.remove": "Anhang entfernen",
"prompt.action.send": "Senden",
"prompt.action.stop": "Stopp",
@@ -204,6 +210,9 @@ export const dict = {
"dialog.mcp.description": "{{enabled}} von {{total}} aktiviert",
"dialog.mcp.empty": "Keine MCPs konfiguriert",
"dialog.lsp.empty": "LSPs automatisch nach Dateityp erkannt",
"dialog.plugins.empty": "In opencode.json konfigurierte Plugins",
"mcp.status.connected": "verbunden",
"mcp.status.failed": "fehlgeschlagen",
"mcp.status.needs_auth": "benötigt Authentifizierung",
@@ -223,13 +232,21 @@ export const dict = {
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "Verbindung zum Server fehlgeschlagen",
"dialog.server.add.checking": "Prüfen...",
"dialog.server.add.button": "Hinzufügen",
"dialog.server.add.button": "Server hinzufügen",
"dialog.server.default.title": "Standardserver",
"dialog.server.default.description":
"Beim App-Start mit diesem Server verbinden, anstatt einen lokalen Server zu starten. Erfordert Neustart.",
"dialog.server.default.none": "Kein Server ausgewählt",
"dialog.server.default.set": "Aktuellen Server als Standard setzen",
"dialog.server.default.clear": "Löschen",
"dialog.server.action.remove": "Server entfernen",
"dialog.server.menu.edit": "Bearbeiten",
"dialog.server.menu.default": "Als Standard festlegen",
"dialog.server.menu.defaultRemove": "Standard entfernen",
"dialog.server.menu.delete": "Löschen",
"dialog.server.current": "Aktueller Server",
"dialog.server.status.default": "Standard",
"dialog.project.edit.title": "Projekt bearbeiten",
"dialog.project.edit.name": "Name",
@@ -238,6 +255,7 @@ export const dict = {
"dialog.project.edit.icon.hint": "Klicken oder Bild ziehen",
"dialog.project.edit.icon.recommended": "Empfohlen: 128x128px",
"dialog.project.edit.color": "Farbe",
"dialog.project.edit.color.select": "{{color}}-Farbe auswählen",
"context.breakdown.title": "Kontext-Aufschlüsselung",
"context.breakdown.note":
@@ -272,15 +290,22 @@ export const dict = {
"context.usage.usage": "Nutzung",
"context.usage.cost": "Kosten",
"context.usage.clickToView": "Klicken, um Kontext anzuzeigen",
"context.usage.view": "Kontextnutzung anzeigen",
"language.en": "Englisch",
"language.zh": "Chinesisch",
"language.ko": "Koreanisch",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Spanisch",
"language.fr": "Französisch",
"language.ja": "Japanisch",
"language.da": "Dänisch",
"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)",
"toast.language.title": "Sprache",
"toast.language.description": "Zu {{language}} gewechselt",
@@ -372,6 +397,7 @@ export const dict = {
"session.tab.session": "Sitzung",
"session.tab.review": "Überprüfung",
"session.tab.context": "Kontext",
"session.panel.reviewAndFiles": "Überprüfung und Dateien",
"session.review.filesChanged": "{{count}} Dateien geändert",
"session.review.loadingChanges": "Lade Änderungen...",
"session.review.empty": "Noch keine Änderungen in dieser Sitzung",
@@ -388,6 +414,15 @@ export const dict = {
"session.new.lastModified": "Zuletzt geändert",
"session.header.search.placeholder": "{{project}} durchsuchen",
"session.header.searchFiles": "Dateien suchen",
"status.popover.trigger": "Status",
"status.popover.ariaLabel": "Serverkonfigurationen",
"status.popover.tab.servers": "Server",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Server verwalten",
"session.share.popover.title": "Im Web veröffentlichen",
"session.share.popover.description.shared":
@@ -410,6 +445,7 @@ export const dict = {
"terminal.loading": "Lade Terminal...",
"terminal.title": "Terminal",
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Terminal schließen",
"common.closeTab": "Tab schließen",
"common.dismiss": "Verwerfen",
@@ -418,11 +454,13 @@ export const dict = {
"common.learnMore": "Mehr erfahren",
"common.rename": "Umbenennen",
"common.reset": "Zurücksetzen",
"common.archive": "Archivieren",
"common.delete": "Löschen",
"common.close": "Schließen",
"common.edit": "Bearbeiten",
"common.loadMore": "Mehr laden",
"sidebar.nav.projectsAndSessions": "Projekte und Sitzungen",
"sidebar.settings": "Einstellungen",
"sidebar.help": "Hilfe",
"sidebar.workspaces.enable": "Arbeitsbereiche aktivieren",
@@ -539,6 +577,11 @@ export const dict = {
"settings.permissions.tool.doom_loop.title": "Doom Loop",
"settings.permissions.tool.doom_loop.description": "Wiederholte Tool-Aufrufe mit identischer Eingabe erkennen",
"session.delete.failed.title": "Sitzung konnte nicht gelöscht werden",
"session.delete.title": "Sitzung löschen",
"session.delete.confirm": 'Sitzung "{{name}}" löschen?',
"session.delete.button": "Sitzung löschen",
"workspace.new": "Neuer Arbeitsbereich",
"workspace.type.local": "lokal",
"workspace.type.sandbox": "Sandbox",

View File

@@ -88,6 +88,8 @@ export const dict = {
"dialog.provider.group.other": "Other",
"dialog.provider.tag.recommended": "Recommended",
"dialog.provider.anthropic.note": "Connect with Claude Pro/Max or API key",
"dialog.provider.openai.note": "Connect with ChatGPT Pro/Plus or API key",
"dialog.provider.copilot.note": "Connect with Copilot or API key",
"dialog.model.select.title": "Select model",
"dialog.model.search.placeholder": "Search models",
@@ -98,7 +100,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode",
"dialog.model.unpaid.addMore.title": "Add more models from popular providers",
"dialog.provider.viewAll": "View all providers",
"dialog.provider.viewAll": "Show more providers",
"provider.connect.title": "Connect {{provider}}",
"provider.connect.title.anthropicProMax": "Login with Claude Pro/Max",
@@ -135,6 +137,9 @@ export const dict = {
"provider.connect.toast.connected.title": "{{provider}} connected",
"provider.connect.toast.connected.description": "{{provider}} models are now available to use.",
"provider.disconnect.toast.disconnected.title": "{{provider}} disconnected",
"provider.disconnect.toast.disconnected.description": "{{provider}} models are no longer available.",
"model.tag.free": "Free",
"model.tag.latest": "Latest",
"model.provider.anthropic": "Anthropic",
@@ -153,9 +158,12 @@ export const dict = {
"model.tooltip.context": "Context limit {{limit}}",
"common.search.placeholder": "Search",
"common.goBack": "Go back",
"common.loading": "Loading",
"common.loading.ellipsis": "...",
"common.cancel": "Cancel",
"common.connect": "Connect",
"common.disconnect": "Disconnect",
"common.submit": "Submit",
"common.save": "Save",
"common.saving": "Saving...",
@@ -199,7 +207,10 @@ export const dict = {
"prompt.slash.badge.custom": "custom",
"prompt.context.active": "active",
"prompt.context.includeActiveFile": "Include active file",
"prompt.context.removeActiveFile": "Remove active file from context",
"prompt.context.removeFile": "Remove file from context",
"prompt.action.attachFile": "Attach file",
"prompt.attachment.remove": "Remove attachment",
"prompt.action.send": "Send",
"prompt.action.stop": "Stop",
@@ -217,6 +228,9 @@ export const dict = {
"dialog.mcp.description": "{{enabled}} of {{total}} enabled",
"dialog.mcp.empty": "No MCPs configured",
"dialog.lsp.empty": "LSPs auto-detected from file types",
"dialog.plugins.empty": "Plugins configured in opencode.json",
"mcp.status.connected": "connected",
"mcp.status.failed": "failed",
"mcp.status.needs_auth": "needs auth",
@@ -236,13 +250,21 @@ export const dict = {
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "Could not connect to server",
"dialog.server.add.checking": "Checking...",
"dialog.server.add.button": "Add",
"dialog.server.add.button": "Add server",
"dialog.server.default.title": "Default server",
"dialog.server.default.description":
"Connect to this server on app launch instead of starting a local server. Requires restart.",
"dialog.server.default.none": "No server selected",
"dialog.server.default.set": "Set current server as default",
"dialog.server.default.clear": "Clear",
"dialog.server.action.remove": "Remove server",
"dialog.server.menu.edit": "Edit",
"dialog.server.menu.default": "Set as default",
"dialog.server.menu.defaultRemove": "Remove default",
"dialog.server.menu.delete": "Delete",
"dialog.server.current": "Current Server",
"dialog.server.status.default": "Default",
"dialog.project.edit.title": "Edit project",
"dialog.project.edit.name": "Name",
@@ -251,6 +273,10 @@ export const dict = {
"dialog.project.edit.icon.hint": "Click or drag an image",
"dialog.project.edit.icon.recommended": "Recommended: 128x128px",
"dialog.project.edit.color": "Color",
"dialog.project.edit.color.select": "Select {{color}} color",
"dialog.project.edit.worktree.startup": "Workspace startup script",
"dialog.project.edit.worktree.startup.description": "Runs after creating a new workspace (worktree).",
"dialog.project.edit.worktree.startup.placeholder": "e.g. bun install",
"context.breakdown.title": "Context Breakdown",
"context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.',
@@ -284,15 +310,22 @@ export const dict = {
"context.usage.usage": "Usage",
"context.usage.cost": "Cost",
"context.usage.clickToView": "Click to view context",
"context.usage.view": "View context usage",
"language.en": "English",
"language.zh": "Chinese",
"language.ko": "Korean",
"language.de": "German",
"language.es": "Spanish",
"language.fr": "French",
"language.ja": "Japanese",
"language.da": "Danish",
"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)",
"toast.language.title": "Language",
"toast.language.description": "Switched to {{language}}",
@@ -382,6 +415,7 @@ export const dict = {
"session.tab.session": "Session",
"session.tab.review": "Review",
"session.tab.context": "Context",
"session.panel.reviewAndFiles": "Review and files",
"session.review.filesChanged": "{{count}} Files Changed",
"session.review.loadingChanges": "Loading changes...",
"session.review.empty": "No changes in this session yet",
@@ -399,6 +433,15 @@ export const dict = {
"session.new.lastModified": "Last modified",
"session.header.search.placeholder": "Search {{project}}",
"session.header.searchFiles": "Search files",
"status.popover.trigger": "Status",
"status.popover.ariaLabel": "Server configurations",
"status.popover.tab.servers": "Servers",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Manage servers",
"session.share.popover.title": "Publish on web",
"session.share.popover.description.shared":
@@ -421,6 +464,7 @@ export const dict = {
"terminal.loading": "Loading terminal...",
"terminal.title": "Terminal",
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Close terminal",
"terminal.connectionLost.title": "Connection Lost",
"terminal.connectionLost.description":
"The terminal connection was interrupted. This can happen when the server restarts.",
@@ -432,6 +476,7 @@ export const dict = {
"common.learnMore": "Learn more",
"common.rename": "Rename",
"common.reset": "Reset",
"common.archive": "Archive",
"common.delete": "Delete",
"common.close": "Close",
"common.edit": "Edit",
@@ -439,6 +484,7 @@ export const dict = {
"common.key.esc": "ESC",
"sidebar.menu.toggle": "Toggle menu",
"sidebar.nav.projectsAndSessions": "Projects and sessions",
"sidebar.settings": "Settings",
"sidebar.help": "Help",
"sidebar.workspaces.enable": "Enable workspaces",
@@ -450,6 +496,7 @@ export const dict = {
"sidebar.project.viewAllSessions": "View all sessions",
"settings.section.desktop": "Desktop",
"settings.section.server": "Server",
"settings.tab.general": "General",
"settings.tab.shortcuts": "Shortcuts",
@@ -471,6 +518,7 @@ export const dict = {
"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",
@@ -557,6 +605,13 @@ export const dict = {
"settings.providers.title": "Providers",
"settings.providers.description": "Provider settings will be configurable here.",
"settings.providers.section.connected": "Connected providers",
"settings.providers.connected.empty": "No connected providers",
"settings.providers.section.popular": "Popular providers",
"settings.providers.tag.environment": "Environment",
"settings.providers.tag.config": "Config",
"settings.providers.tag.custom": "Custom",
"settings.providers.tag.other": "Other",
"settings.models.title": "Models",
"settings.models.description": "Model settings will be configurable here.",
"settings.agents.title": "Agents",
@@ -608,6 +663,11 @@ export const dict = {
"settings.permissions.tool.doom_loop.title": "Doom Loop",
"settings.permissions.tool.doom_loop.description": "Detect repeated tool calls with identical input",
"session.delete.failed.title": "Failed to delete session",
"session.delete.title": "Delete session",
"session.delete.confirm": 'Delete session "{{name}}"?',
"session.delete.button": "Delete session",
"workspace.new": "New workspace",
"workspace.type.local": "local",
"workspace.type.sandbox": "sandbox",

View File

@@ -86,6 +86,8 @@ export const dict = {
"dialog.provider.group.other": "Otro",
"dialog.provider.tag.recommended": "Recomendado",
"dialog.provider.anthropic.note": "Conectar con Claude Pro/Max o clave API",
"dialog.provider.openai.note": "Conectar con ChatGPT Pro/Plus o clave API",
"dialog.provider.copilot.note": "Conectar con Copilot o clave API",
"dialog.model.select.title": "Seleccionar modelo",
"dialog.model.search.placeholder": "Buscar modelos",
@@ -96,7 +98,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por OpenCode",
"dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares",
"dialog.provider.viewAll": "Ver todos los proveedores",
"dialog.provider.viewAll": "Ver s proveedores",
"provider.connect.title": "Conectar {{provider}}",
"provider.connect.title.anthropicProMax": "Iniciar sesión con Claude Pro/Max",
@@ -136,6 +138,7 @@ export const dict = {
"model.tag.latest": "Último",
"common.search.placeholder": "Buscar",
"common.goBack": "Volver",
"common.loading": "Cargando",
"common.cancel": "Cancelar",
"common.submit": "Enviar",
@@ -181,7 +184,10 @@ export const dict = {
"prompt.slash.badge.custom": "personalizado",
"prompt.context.active": "activo",
"prompt.context.includeActiveFile": "Incluir archivo activo",
"prompt.context.removeActiveFile": "Eliminar archivo activo del contexto",
"prompt.context.removeFile": "Eliminar archivo del contexto",
"prompt.action.attachFile": "Adjuntar archivo",
"prompt.attachment.remove": "Eliminar adjunto",
"prompt.action.send": "Enviar",
"prompt.action.stop": "Detener",
@@ -199,6 +205,9 @@ export const dict = {
"dialog.mcp.description": "{{enabled}} de {{total}} habilitados",
"dialog.mcp.empty": "No hay MCPs configurados",
"dialog.lsp.empty": "LSPs detectados automáticamente por tipo de archivo",
"dialog.plugins.empty": "Plugins configurados en opencode.json",
"mcp.status.connected": "conectado",
"mcp.status.failed": "fallido",
"mcp.status.needs_auth": "necesita auth",
@@ -218,13 +227,21 @@ export const dict = {
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "No se pudo conectar al servidor",
"dialog.server.add.checking": "Comprobando...",
"dialog.server.add.button": "Añadir",
"dialog.server.add.button": "Añadir servidor",
"dialog.server.default.title": "Servidor predeterminado",
"dialog.server.default.description":
"Conectar a este servidor al iniciar la app en lugar de iniciar un servidor local. Requiere reinicio.",
"dialog.server.default.none": "Ningún servidor seleccionado",
"dialog.server.default.set": "Establecer servidor actual como predeterminado",
"dialog.server.default.clear": "Limpiar",
"dialog.server.action.remove": "Eliminar servidor",
"dialog.server.menu.edit": "Editar",
"dialog.server.menu.default": "Establecer como predeterminado",
"dialog.server.menu.defaultRemove": "Quitar predeterminado",
"dialog.server.menu.delete": "Eliminar",
"dialog.server.current": "Servidor actual",
"dialog.server.status.default": "Predeterminado",
"dialog.project.edit.title": "Editar proyecto",
"dialog.project.edit.name": "Nombre",
@@ -233,6 +250,7 @@ export const dict = {
"dialog.project.edit.icon.hint": "Haz clic o arrastra una imagen",
"dialog.project.edit.icon.recommended": "Recomendado: 128x128px",
"dialog.project.edit.color": "Color",
"dialog.project.edit.color.select": "Seleccionar color {{color}}",
"context.breakdown.title": "Desglose de Contexto",
"context.breakdown.note":
@@ -267,15 +285,22 @@ export const dict = {
"context.usage.usage": "Uso",
"context.usage.cost": "Costo",
"context.usage.clickToView": "Haz clic para ver contexto",
"context.usage.view": "Ver uso del contexto",
"language.en": "Inglés",
"language.zh": "Chino",
"language.ko": "Coreano",
"language.de": "Alemán",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Frans",
"language.ja": "Japonés",
"language.da": "Danés",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"toast.language.title": "Idioma",
"toast.language.description": "Cambiado a {{language}}",
@@ -366,6 +391,7 @@ export const dict = {
"session.tab.session": "Sesión",
"session.tab.review": "Revisión",
"session.tab.context": "Contexto",
"session.panel.reviewAndFiles": "Revisión y archivos",
"session.review.filesChanged": "{{count}} Archivos Cambiados",
"session.review.loadingChanges": "Cargando cambios...",
"session.review.empty": "No hay cambios en esta sesión aún",
@@ -382,6 +408,15 @@ export const dict = {
"session.new.lastModified": "Última modificación",
"session.header.search.placeholder": "Buscar {{project}}",
"session.header.searchFiles": "Buscar archivos",
"status.popover.trigger": "Estado",
"status.popover.ariaLabel": "Configuraciones del servidor",
"status.popover.tab.servers": "Servidores",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Administrar servidores",
"session.share.popover.title": "Publicar en web",
"session.share.popover.description.shared":
@@ -404,6 +439,7 @@ export const dict = {
"terminal.loading": "Cargando terminal...",
"terminal.title": "Terminal",
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Cerrar terminal",
"common.closeTab": "Cerrar pestaña",
"common.dismiss": "Descartar",
@@ -412,11 +448,13 @@ export const dict = {
"common.learnMore": "Saber más",
"common.rename": "Renombrar",
"common.reset": "Restablecer",
"common.archive": "Archivar",
"common.delete": "Eliminar",
"common.close": "Cerrar",
"common.edit": "Editar",
"common.loadMore": "Cargar más",
"sidebar.nav.projectsAndSessions": "Proyectos y sesiones",
"sidebar.settings": "Ajustes",
"sidebar.help": "Ayuda",
"sidebar.workspaces.enable": "Habilitar espacios de trabajo",
@@ -533,6 +571,11 @@ export const dict = {
"settings.permissions.tool.doom_loop.title": "Bucle Infinito",
"settings.permissions.tool.doom_loop.description": "Detectar llamadas a herramientas repetidas con entrada idéntica",
"session.delete.failed.title": "Fallo al eliminar sesión",
"session.delete.title": "Eliminar sesión",
"session.delete.confirm": '¿Eliminar sesión "{{name}}"?',
"session.delete.button": "Eliminar sesión",
"workspace.new": "Nuevo espacio de trabajo",
"workspace.type.local": "local",
"workspace.type.sandbox": "sandbox",

View File

@@ -86,6 +86,8 @@ export const dict = {
"dialog.provider.group.other": "Autre",
"dialog.provider.tag.recommended": "Recommandé",
"dialog.provider.anthropic.note": "Connectez-vous avec Claude Pro/Max ou une clé API",
"dialog.provider.openai.note": "Connectez-vous avec ChatGPT Pro/Plus ou une clé API",
"dialog.provider.copilot.note": "Connectez-vous avec Copilot ou une clé API",
"dialog.model.select.title": "Sélectionner un modèle",
"dialog.model.search.placeholder": "Rechercher des modèles",
@@ -96,7 +98,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode",
"dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires",
"dialog.provider.viewAll": "Voir tous les fournisseurs",
"dialog.provider.viewAll": "Voir plus de fournisseurs",
"provider.connect.title": "Connecter {{provider}}",
"provider.connect.title.anthropicProMax": "Connexion avec Claude Pro/Max",
@@ -136,6 +138,7 @@ export const dict = {
"model.tag.latest": "Dernier",
"common.search.placeholder": "Rechercher",
"common.goBack": "Retour",
"common.loading": "Chargement",
"common.cancel": "Annuler",
"common.submit": "Soumettre",
@@ -181,7 +184,10 @@ export const dict = {
"prompt.slash.badge.custom": "personnalisé",
"prompt.context.active": "actif",
"prompt.context.includeActiveFile": "Inclure le fichier actif",
"prompt.context.removeActiveFile": "Retirer le fichier actif du contexte",
"prompt.context.removeFile": "Retirer le fichier du contexte",
"prompt.action.attachFile": "Joindre un fichier",
"prompt.attachment.remove": "Supprimer la pièce jointe",
"prompt.action.send": "Envoyer",
"prompt.action.stop": "Arrêter",
@@ -199,6 +205,9 @@ export const dict = {
"dialog.mcp.description": "{{enabled}} sur {{total}} activés",
"dialog.mcp.empty": "Aucun MCP configuré",
"dialog.lsp.empty": "LSPs détectés automatiquement par type de fichier",
"dialog.plugins.empty": "Plugins configurés dans opencode.json",
"mcp.status.connected": "connecté",
"mcp.status.failed": "échoué",
"mcp.status.needs_auth": "nécessite auth",
@@ -218,13 +227,21 @@ export const dict = {
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "Impossible de se connecter au serveur",
"dialog.server.add.checking": "Vérification...",
"dialog.server.add.button": "Ajouter",
"dialog.server.add.button": "Ajouter un serveur",
"dialog.server.default.title": "Serveur par défaut",
"dialog.server.default.description":
"Se connecter à ce serveur au lancement de l'application au lieu de démarrer un serveur local. Nécessite un redémarrage.",
"dialog.server.default.none": "Aucun serveur sélectionné",
"dialog.server.default.set": "Définir le serveur actuel comme défaut",
"dialog.server.default.clear": "Effacer",
"dialog.server.action.remove": "Supprimer le serveur",
"dialog.server.menu.edit": "Modifier",
"dialog.server.menu.default": "Définir par défaut",
"dialog.server.menu.defaultRemove": "Supprimer par défaut",
"dialog.server.menu.delete": "Supprimer",
"dialog.server.current": "Serveur actuel",
"dialog.server.status.default": "Défaut",
"dialog.project.edit.title": "Modifier le projet",
"dialog.project.edit.name": "Nom",
@@ -233,6 +250,7 @@ export const dict = {
"dialog.project.edit.icon.hint": "Cliquez ou faites glisser une image",
"dialog.project.edit.icon.recommended": "Recommandé : 128x128px",
"dialog.project.edit.color": "Couleur",
"dialog.project.edit.color.select": "Sélectionner la couleur {{color}}",
"context.breakdown.title": "Répartition du contexte",
"context.breakdown.note":
@@ -267,15 +285,22 @@ export const dict = {
"context.usage.usage": "Utilisation",
"context.usage.cost": "Coût",
"context.usage.clickToView": "Cliquez pour voir le contexte",
"context.usage.view": "Voir l'utilisation du contexte",
"language.en": "Anglais",
"language.zh": "Chinois",
"language.ko": "Coréen",
"language.de": "Allemand",
"language.es": "Espagnol",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.ja": "Japonais",
"language.da": "Danois",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"toast.language.title": "Langue",
"toast.language.description": "Passé à {{language}}",
@@ -371,6 +396,7 @@ export const dict = {
"session.tab.session": "Session",
"session.tab.review": "Revue",
"session.tab.context": "Contexte",
"session.panel.reviewAndFiles": "Revue et fichiers",
"session.review.filesChanged": "{{count}} fichiers modifiés",
"session.review.loadingChanges": "Chargement des modifications...",
"session.review.empty": "Aucune modification dans cette session pour l'instant",
@@ -387,6 +413,15 @@ export const dict = {
"session.new.lastModified": "Dernière modification",
"session.header.search.placeholder": "Rechercher {{project}}",
"session.header.searchFiles": "Rechercher des fichiers",
"status.popover.trigger": "Statut",
"status.popover.ariaLabel": "Configurations des serveurs",
"status.popover.tab.servers": "Serveurs",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Gérer les serveurs",
"session.share.popover.title": "Publier sur le web",
"session.share.popover.description.shared":
@@ -409,6 +444,7 @@ export const dict = {
"terminal.loading": "Chargement du terminal...",
"terminal.title": "Terminal",
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Fermer le terminal",
"common.closeTab": "Fermer l'onglet",
"common.dismiss": "Ignorer",
@@ -417,11 +453,13 @@ export const dict = {
"common.learnMore": "En savoir plus",
"common.rename": "Renommer",
"common.reset": "Réinitialiser",
"common.archive": "Archiver",
"common.delete": "Supprimer",
"common.close": "Fermer",
"common.edit": "Modifier",
"common.loadMore": "Charger plus",
"sidebar.nav.projectsAndSessions": "Projets et sessions",
"sidebar.settings": "Paramètres",
"sidebar.help": "Aide",
"sidebar.workspaces.enable": "Activer les espaces de travail",
@@ -540,6 +578,11 @@ export const dict = {
"settings.permissions.tool.doom_loop.title": "Boucle infernale",
"settings.permissions.tool.doom_loop.description": "Détecter les appels d'outils répétés avec une entrée identique",
"session.delete.failed.title": "Échec de la suppression de la session",
"session.delete.title": "Supprimer la session",
"session.delete.confirm": 'Supprimer la session "{{name}}" ?',
"session.delete.button": "Supprimer la session",
"workspace.new": "Nouvel espace de travail",
"workspace.type.local": "local",
"workspace.type.sandbox": "bac à sable",

View File

@@ -86,6 +86,8 @@ export const dict = {
"dialog.provider.group.other": "その他",
"dialog.provider.tag.recommended": "推奨",
"dialog.provider.anthropic.note": "Claude Pro/MaxまたはAPIキーで接続",
"dialog.provider.openai.note": "ChatGPT Pro/PlusまたはAPIキーで接続",
"dialog.provider.copilot.note": "CopilotまたはAPIキーで接続",
"dialog.model.select.title": "モデルを選択",
"dialog.model.search.placeholder": "モデルを検索",
@@ -96,7 +98,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "OpenCodeが提供する無料モデル",
"dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加",
"dialog.provider.viewAll": "すべてのプロバイダーを表示",
"dialog.provider.viewAll": "さらにプロバイダーを表示",
"provider.connect.title": "{{provider}}を接続",
"provider.connect.title.anthropicProMax": "Claude Pro/Maxでログイン",
@@ -135,6 +137,7 @@ export const dict = {
"model.tag.latest": "最新",
"common.search.placeholder": "検索",
"common.goBack": "戻る",
"common.loading": "読み込み中",
"common.cancel": "キャンセル",
"common.submit": "送信",
@@ -180,7 +183,10 @@ export const dict = {
"prompt.slash.badge.custom": "カスタム",
"prompt.context.active": "アクティブ",
"prompt.context.includeActiveFile": "アクティブなファイルを含める",
"prompt.context.removeActiveFile": "コンテキストからアクティブなファイルを削除",
"prompt.context.removeFile": "コンテキストからファイルを削除",
"prompt.action.attachFile": "ファイルを添付",
"prompt.attachment.remove": "添付ファイルを削除",
"prompt.action.send": "送信",
"prompt.action.stop": "停止",
@@ -198,6 +204,9 @@ export const dict = {
"dialog.mcp.description": "{{total}}個中{{enabled}}個が有効",
"dialog.mcp.empty": "MCPが設定されていません",
"dialog.lsp.empty": "ファイルタイプから自動検出されたLSP",
"dialog.plugins.empty": "opencode.jsonで設定されたプラグイン",
"mcp.status.connected": "接続済み",
"mcp.status.failed": "失敗",
"mcp.status.needs_auth": "認証が必要",
@@ -217,13 +226,21 @@ export const dict = {
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "サーバーに接続できませんでした",
"dialog.server.add.checking": "確認中...",
"dialog.server.add.button": "追加",
"dialog.server.add.button": "サーバーを追加",
"dialog.server.default.title": "デフォルトサーバー",
"dialog.server.default.description":
"ローカルサーバーを起動する代わりに、アプリ起動時にこのサーバーに接続します。再起動が必要です。",
"dialog.server.default.none": "サーバーが選択されていません",
"dialog.server.default.set": "現在のサーバーをデフォルトに設定",
"dialog.server.default.clear": "クリア",
"dialog.server.action.remove": "サーバーを削除",
"dialog.server.menu.edit": "編集",
"dialog.server.menu.default": "デフォルトに設定",
"dialog.server.menu.defaultRemove": "デフォルト設定を解除",
"dialog.server.menu.delete": "削除",
"dialog.server.current": "現在のサーバー",
"dialog.server.status.default": "デフォルト",
"dialog.project.edit.title": "プロジェクトを編集",
"dialog.project.edit.name": "名前",
@@ -232,6 +249,7 @@ export const dict = {
"dialog.project.edit.icon.hint": "クリックまたは画像をドラッグ",
"dialog.project.edit.icon.recommended": "推奨: 128x128px",
"dialog.project.edit.color": "色",
"dialog.project.edit.color.select": "{{color}}の色を選択",
"context.breakdown.title": "コンテキストの内訳",
"context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。',
@@ -265,15 +283,22 @@ export const dict = {
"context.usage.usage": "使用量",
"context.usage.cost": "コスト",
"context.usage.clickToView": "クリックしてコンテキストを表示",
"context.usage.view": "コンテキスト使用量を表示",
"language.en": "英語",
"language.zh": "中国語",
"language.ko": "韓国語",
"language.de": "ドイツ語",
"language.es": "スペイン語",
"language.fr": "フランス語",
"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.da": "デンマーク語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"toast.language.title": "言語",
"toast.language.description": "{{language}}に切り替えました",
@@ -363,6 +388,7 @@ export const dict = {
"session.tab.session": "セッション",
"session.tab.review": "レビュー",
"session.tab.context": "コンテキスト",
"session.panel.reviewAndFiles": "レビューとファイル",
"session.review.filesChanged": "{{count}} ファイル変更",
"session.review.loadingChanges": "変更を読み込み中...",
"session.review.empty": "このセッションでの変更はまだありません",
@@ -379,6 +405,15 @@ export const dict = {
"session.new.lastModified": "最終更新",
"session.header.search.placeholder": "{{project}}を検索",
"session.header.searchFiles": "ファイルを検索",
"status.popover.trigger": "ステータス",
"status.popover.ariaLabel": "サーバー設定",
"status.popover.tab.servers": "サーバー",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "プラグイン",
"status.popover.action.manageServers": "サーバーを管理",
"session.share.popover.title": "ウェブで公開",
"session.share.popover.description.shared":
@@ -401,6 +436,7 @@ export const dict = {
"terminal.loading": "ターミナルを読み込み中...",
"terminal.title": "ターミナル",
"terminal.title.numbered": "ターミナル {{number}}",
"terminal.close": "ターミナルを閉じる",
"common.closeTab": "タブを閉じる",
"common.dismiss": "閉じる",
@@ -409,11 +445,13 @@ export const dict = {
"common.learnMore": "詳細",
"common.rename": "名前変更",
"common.reset": "リセット",
"common.archive": "アーカイブ",
"common.delete": "削除",
"common.close": "閉じる",
"common.edit": "編集",
"common.loadMore": "さらに読み込む",
"sidebar.nav.projectsAndSessions": "プロジェクトとセッション",
"sidebar.settings": "設定",
"sidebar.help": "ヘルプ",
"sidebar.workspaces.enable": "ワークスペースを有効化",
@@ -527,6 +565,11 @@ export const dict = {
"settings.permissions.tool.doom_loop.title": "Doom Loop",
"settings.permissions.tool.doom_loop.description": "同一入力による繰り返しのツール呼び出しを検出",
"session.delete.failed.title": "セッションの削除に失敗しました",
"session.delete.title": "セッションの削除",
"session.delete.confirm": 'セッション "{{name}}" を削除しますか?',
"session.delete.button": "セッションを削除",
"workspace.new": "新しいワークスペース",
"workspace.type.local": "ローカル",
"workspace.type.sandbox": "サンドボックス",

View File

@@ -90,6 +90,8 @@ export const dict = {
"dialog.provider.group.other": "기타",
"dialog.provider.tag.recommended": "추천",
"dialog.provider.anthropic.note": "Claude Pro/Max 또는 API 키로 연결",
"dialog.provider.openai.note": "ChatGPT Pro/Plus 또는 API 키로 연결",
"dialog.provider.copilot.note": "Copilot 또는 API 키로 연결",
"dialog.model.select.title": "모델 선택",
"dialog.model.search.placeholder": "모델 검색",
@@ -100,7 +102,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "OpenCode에서 제공하는 무료 모델",
"dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가",
"dialog.provider.viewAll": "모든 공급자 보기",
"dialog.provider.viewAll": "더 많은 공급자 보기",
"provider.connect.title": "{{provider}} 연결",
"provider.connect.title.anthropicProMax": "Claude Pro/Max로 로그인",
@@ -139,6 +141,7 @@ export const dict = {
"model.tag.latest": "최신",
"common.search.placeholder": "검색",
"common.goBack": "뒤로 가기",
"common.loading": "로딩 중",
"common.cancel": "취소",
"common.submit": "제출",
@@ -184,7 +187,10 @@ export const dict = {
"prompt.slash.badge.custom": "사용자 지정",
"prompt.context.active": "활성",
"prompt.context.includeActiveFile": "활성 파일 포함",
"prompt.context.removeActiveFile": "컨텍스트에서 활성 파일 제거",
"prompt.context.removeFile": "컨텍스트에서 파일 제거",
"prompt.action.attachFile": "파일 첨부",
"prompt.attachment.remove": "첨부 파일 제거",
"prompt.action.send": "전송",
"prompt.action.stop": "중지",
@@ -202,6 +208,9 @@ export const dict = {
"dialog.mcp.description": "{{total}}개 중 {{enabled}}개 활성화됨",
"dialog.mcp.empty": "구성된 MCP 없음",
"dialog.lsp.empty": "파일 유형에서 자동 감지된 LSP",
"dialog.plugins.empty": "opencode.json에 구성된 플러그인",
"mcp.status.connected": "연결됨",
"mcp.status.failed": "실패",
"mcp.status.needs_auth": "인증 필요",
@@ -221,13 +230,21 @@ export const dict = {
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "서버에 연결할 수 없습니다",
"dialog.server.add.checking": "확인 중...",
"dialog.server.add.button": "추가",
"dialog.server.add.button": "서버 추가",
"dialog.server.default.title": "기본 서버",
"dialog.server.default.description":
"로컬 서버를 시작하는 대신 앱 실행 시 이 서버에 연결합니다. 다시 시작해야 합니다.",
"dialog.server.default.none": "선택된 서버 없음",
"dialog.server.default.set": "현재 서버를 기본값으로 설정",
"dialog.server.default.clear": "지우기",
"dialog.server.action.remove": "서버 제거",
"dialog.server.menu.edit": "편집",
"dialog.server.menu.default": "기본값으로 설정",
"dialog.server.menu.defaultRemove": "기본값 제거",
"dialog.server.menu.delete": "삭제",
"dialog.server.current": "현재 서버",
"dialog.server.status.default": "기본값",
"dialog.project.edit.title": "프로젝트 편집",
"dialog.project.edit.name": "이름",
@@ -236,6 +253,7 @@ export const dict = {
"dialog.project.edit.icon.hint": "이미지를 클릭하거나 드래그하세요",
"dialog.project.edit.icon.recommended": "권장: 128x128px",
"dialog.project.edit.color": "색상",
"dialog.project.edit.color.select": "{{color}} 색상 선택",
"context.breakdown.title": "컨텍스트 분석",
"context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.',
@@ -269,15 +287,22 @@ export const dict = {
"context.usage.usage": "사용량",
"context.usage.cost": "비용",
"context.usage.clickToView": "컨텍스트를 보려면 클릭",
"context.usage.view": "컨텍스트 사용량 보기",
"language.en": "영어",
"language.zh": "중국어",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "독일어",
"language.es": "스페인어",
"language.fr": "프랑스어",
"language.ja": "일본어",
"language.da": "덴마크어",
"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)",
"toast.language.title": "언어",
"toast.language.description": "{{language}}(으)로 전환됨",
@@ -366,6 +391,7 @@ export const dict = {
"session.tab.session": "세션",
"session.tab.review": "검토",
"session.tab.context": "컨텍스트",
"session.panel.reviewAndFiles": "검토 및 파일",
"session.review.filesChanged": "{{count}}개 파일 변경됨",
"session.review.loadingChanges": "변경 사항 로드 중...",
"session.review.empty": "이 세션에 변경 사항이 아직 없습니다",
@@ -382,6 +408,15 @@ export const dict = {
"session.new.lastModified": "최근 수정",
"session.header.search.placeholder": "{{project}} 검색",
"session.header.searchFiles": "파일 검색",
"status.popover.trigger": "상태",
"status.popover.ariaLabel": "서버 구성",
"status.popover.tab.servers": "서버",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "플러그인",
"status.popover.action.manageServers": "서버 관리",
"session.share.popover.title": "웹에 게시",
"session.share.popover.description.shared": "이 세션은 웹에 공개되었습니다. 링크가 있는 누구나 액세스할 수 있습니다.",
@@ -403,6 +438,7 @@ export const dict = {
"terminal.loading": "터미널 로드 중...",
"terminal.title": "터미널",
"terminal.title.numbered": "터미널 {{number}}",
"terminal.close": "터미널 닫기",
"common.closeTab": "탭 닫기",
"common.dismiss": "닫기",
@@ -411,11 +447,13 @@ export const dict = {
"common.learnMore": "더 알아보기",
"common.rename": "이름 바꾸기",
"common.reset": "초기화",
"common.archive": "보관",
"common.delete": "삭제",
"common.close": "닫기",
"common.edit": "편집",
"common.loadMore": "더 불러오기",
"sidebar.nav.projectsAndSessions": "프로젝트 및 세션",
"sidebar.settings": "설정",
"sidebar.help": "도움말",
"sidebar.workspaces.enable": "작업 공간 활성화",
@@ -528,6 +566,11 @@ export const dict = {
"settings.permissions.tool.doom_loop.title": "무한 반복",
"settings.permissions.tool.doom_loop.description": "동일한 입력으로 반복되는 도구 호출 감지",
"session.delete.failed.title": "세션 삭제 실패",
"session.delete.title": "세션 삭제",
"session.delete.confirm": '"{{name}}" 세션을 삭제하시겠습니까?',
"session.delete.button": "세션 삭제",
"workspace.new": "새 작업 공간",
"workspace.type.local": "로컬",
"workspace.type.sandbox": "샌드박스",

620
packages/app/src/i18n/no.ts Normal file
View File

@@ -0,0 +1,620 @@
import { dict as en } from "./en"
type Keys = keyof typeof en
export const dict = {
"command.category.suggested": "Foreslått",
"command.category.view": "Visning",
"command.category.project": "Prosjekt",
"command.category.provider": "Leverandør",
"command.category.server": "Server",
"command.category.session": "Sesjon",
"command.category.theme": "Tema",
"command.category.language": "Språk",
"command.category.file": "Fil",
"command.category.terminal": "Terminal",
"command.category.model": "Modell",
"command.category.mcp": "MCP",
"command.category.agent": "Agent",
"command.category.permissions": "Tillatelser",
"command.category.workspace": "Arbeidsområde",
"command.category.settings": "Innstillinger",
"theme.scheme.system": "System",
"theme.scheme.light": "Lys",
"theme.scheme.dark": "Mørk",
"command.sidebar.toggle": "Veksle sidepanel",
"command.project.open": "Åpne prosjekt",
"command.provider.connect": "Koble til leverandør",
"command.server.switch": "Bytt server",
"command.settings.open": "Åpne innstillinger",
"command.session.previous": "Forrige sesjon",
"command.session.next": "Neste sesjon",
"command.session.archive": "Arkiver sesjon",
"command.palette": "Kommandopalett",
"command.theme.cycle": "Bytt tema",
"command.theme.set": "Bruk tema: {{theme}}",
"command.theme.scheme.cycle": "Bytt fargevalg",
"command.theme.scheme.set": "Bruk fargevalg: {{scheme}}",
"command.language.cycle": "Bytt språk",
"command.language.set": "Bruk språk: {{language}}",
"command.session.new": "Ny sesjon",
"command.file.open": "Åpne fil",
"command.file.open.description": "Søk i filer og kommandoer",
"command.terminal.toggle": "Veksle terminal",
"command.review.toggle": "Veksle gjennomgang",
"command.terminal.new": "Ny terminal",
"command.terminal.new.description": "Opprett en ny terminalfane",
"command.steps.toggle": "Veksle trinn",
"command.steps.toggle.description": "Vis eller skjul trinn for gjeldende melding",
"command.message.previous": "Forrige melding",
"command.message.previous.description": "Gå til forrige brukermelding",
"command.message.next": "Neste melding",
"command.message.next.description": "Gå til neste brukermelding",
"command.model.choose": "Velg modell",
"command.model.choose.description": "Velg en annen modell",
"command.mcp.toggle": "Veksle MCP-er",
"command.mcp.toggle.description": "Veksle MCP-er",
"command.agent.cycle": "Bytt agent",
"command.agent.cycle.description": "Bytt til neste agent",
"command.agent.cycle.reverse": "Bytt agent bakover",
"command.agent.cycle.reverse.description": "Bytt til forrige agent",
"command.model.variant.cycle": "Bytt tenkeinnsats",
"command.model.variant.cycle.description": "Bytt til neste innsatsnivå",
"command.permissions.autoaccept.enable": "Godta endringer automatisk",
"command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk",
"command.session.undo": "Angre",
"command.session.undo.description": "Angre siste melding",
"command.session.redo": "Gjør om",
"command.session.redo.description": "Gjør om siste angrede melding",
"command.session.compact": "Komprimer sesjon",
"command.session.compact.description": "Oppsummer sesjonen for å redusere kontekststørrelsen",
"command.session.fork": "Forgren fra melding",
"command.session.fork.description": "Opprett en ny sesjon fra en tidligere melding",
"command.session.share": "Del sesjon",
"command.session.share.description": "Del denne sesjonen og kopier URL-en til utklippstavlen",
"command.session.unshare": "Slutt å dele sesjon",
"command.session.unshare.description": "Slutt å dele denne sesjonen",
"palette.search.placeholder": "Søk i filer og kommandoer",
"palette.empty": "Ingen resultater funnet",
"palette.group.commands": "Kommandoer",
"palette.group.files": "Filer",
"dialog.provider.search.placeholder": "Søk etter leverandører",
"dialog.provider.empty": "Ingen leverandører funnet",
"dialog.provider.group.popular": "Populære",
"dialog.provider.group.other": "Andre",
"dialog.provider.tag.recommended": "Anbefalt",
"dialog.provider.anthropic.note": "Koble til med Claude Pro/Max eller API-nøkkel",
"dialog.provider.openai.note": "Koble til med ChatGPT Pro/Plus eller API-nøkkel",
"dialog.provider.copilot.note": "Koble til med Copilot eller API-nøkkel",
"dialog.model.select.title": "Velg modell",
"dialog.model.search.placeholder": "Søk etter modeller",
"dialog.model.empty": "Ingen modellresultater",
"dialog.model.manage": "Administrer modeller",
"dialog.model.manage.description": "Tilpass hvilke modeller som vises i modellvelgeren.",
"dialog.model.unpaid.freeModels.title": "Gratis modeller levert av OpenCode",
"dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører",
"dialog.provider.viewAll": "Vis flere leverandører",
"provider.connect.title": "Koble til {{provider}}",
"provider.connect.title.anthropicProMax": "Logg inn med Claude Pro/Max",
"provider.connect.selectMethod": "Velg innloggingsmetode for {{provider}}.",
"provider.connect.method.apiKey": "API-nøkkel",
"provider.connect.status.inProgress": "Autorisering pågår...",
"provider.connect.status.waiting": "Venter på autorisering...",
"provider.connect.status.failed": "Autorisering mislyktes: {{error}}",
"provider.connect.apiKey.description":
"Skriv inn din {{provider}} API-nøkkel for å koble til kontoen din og bruke {{provider}}-modeller i OpenCode.",
"provider.connect.apiKey.label": "{{provider}} API-nøkkel",
"provider.connect.apiKey.placeholder": "API-nøkkel",
"provider.connect.apiKey.required": "API-nøkkel er påkrevd",
"provider.connect.opencodeZen.line1":
"OpenCode Zen gir deg tilgang til et utvalg av pålitelige optimaliserte modeller for kodeagenter.",
"provider.connect.opencodeZen.line2":
"Med én enkelt API-nøkkel får du tilgang til modeller som Claude, GPT, Gemini, GLM og flere.",
"provider.connect.opencodeZen.visit.prefix": "Besøk ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": " for å hente API-nøkkelen din.",
"provider.connect.oauth.code.visit.prefix": "Besøk ",
"provider.connect.oauth.code.visit.link": "denne lenken",
"provider.connect.oauth.code.visit.suffix":
" for å hente autorisasjonskoden din for å koble til kontoen din og bruke {{provider}}-modeller i OpenCode.",
"provider.connect.oauth.code.label": "{{method}} autorisasjonskode",
"provider.connect.oauth.code.placeholder": "Autorisasjonskode",
"provider.connect.oauth.code.required": "Autorisasjonskode er påkrevd",
"provider.connect.oauth.code.invalid": "Ugyldig autorisasjonskode",
"provider.connect.oauth.auto.visit.prefix": "Besøk ",
"provider.connect.oauth.auto.visit.link": "denne lenken",
"provider.connect.oauth.auto.visit.suffix":
" og skriv inn koden nedenfor for å koble til kontoen din og bruke {{provider}}-modeller i OpenCode.",
"provider.connect.oauth.auto.confirmationCode": "Bekreftelseskode",
"provider.connect.toast.connected.title": "{{provider}} tilkoblet",
"provider.connect.toast.connected.description": "{{provider}}-modeller er nå tilgjengelige.",
"model.tag.free": "Gratis",
"model.tag.latest": "Nyeste",
"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": "bilde",
"model.input.audio": "lyd",
"model.input.video": "video",
"model.input.pdf": "pdf",
"model.tooltip.allows": "Tillater: {{inputs}}",
"model.tooltip.reasoning.allowed": "Tillater resonnering",
"model.tooltip.reasoning.none": "Ingen resonnering",
"model.tooltip.context": "Kontekstgrense {{limit}}",
"common.search.placeholder": "Søk",
"common.goBack": "Gå tilbake",
"common.loading": "Laster",
"common.loading.ellipsis": "...",
"common.cancel": "Avbryt",
"common.submit": "Send inn",
"common.save": "Lagre",
"common.saving": "Lagrer...",
"common.default": "Standard",
"common.attachment": "vedlegg",
"prompt.placeholder.shell": "Skriv inn shell-kommando...",
"prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"',
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "ESC for å avslutte",
"prompt.example.1": "Fiks en TODO i kodebasen",
"prompt.example.2": "Hva er teknologistabelen i dette prosjektet?",
"prompt.example.3": "Fiks ødelagte tester",
"prompt.example.4": "Forklar hvordan autentisering fungerer",
"prompt.example.5": "Finn og fiks sikkerhetssårbarheter",
"prompt.example.6": "Legg til enhetstester for brukerservicen",
"prompt.example.7": "Refaktorer denne funksjonen for bedre lesbarhet",
"prompt.example.8": "Hva betyr denne feilen?",
"prompt.example.9": "Hjelp meg med å feilsøke dette problemet",
"prompt.example.10": "Generer API-dokumentasjon",
"prompt.example.11": "Optimaliser databasespørringer",
"prompt.example.12": "Legg til inputvalidering",
"prompt.example.13": "Lag en ny komponent for...",
"prompt.example.14": "Hvordan deployer jeg dette prosjektet?",
"prompt.example.15": "Gjennomgå koden min for beste praksis",
"prompt.example.16": "Legg til feilhåndtering i denne funksjonen",
"prompt.example.17": "Forklar dette regex-mønsteret",
"prompt.example.18": "Konverter dette til TypeScript",
"prompt.example.19": "Legg til logging i hele kodebasen",
"prompt.example.20": "Hvilke avhengigheter er utdaterte?",
"prompt.example.21": "Hjelp meg med å skrive et migreringsskript",
"prompt.example.22": "Implementer caching for dette endepunktet",
"prompt.example.23": "Legg til paginering i denne listen",
"prompt.example.24": "Lag en CLI-kommando for...",
"prompt.example.25": "Hvordan fungerer miljøvariabler her?",
"prompt.popover.emptyResults": "Ingen matchende resultater",
"prompt.popover.emptyCommands": "Ingen matchende kommandoer",
"prompt.dropzone.label": "Slipp bilder eller PDF-er her",
"prompt.slash.badge.custom": "egendefinert",
"prompt.context.active": "aktiv",
"prompt.context.includeActiveFile": "Inkluder aktiv fil",
"prompt.context.removeActiveFile": "Fjern aktiv fil fra kontekst",
"prompt.context.removeFile": "Fjern fil fra kontekst",
"prompt.action.attachFile": "Legg ved fil",
"prompt.attachment.remove": "Fjern vedlegg",
"prompt.action.send": "Send",
"prompt.action.stop": "Stopp",
"prompt.toast.pasteUnsupported.title": "Liming ikke støttet",
"prompt.toast.pasteUnsupported.description": "Kun bilder eller PDF-er kan limes inn her.",
"prompt.toast.modelAgentRequired.title": "Velg en agent og modell",
"prompt.toast.modelAgentRequired.description": "Velg en agent og modell før du sender en forespørsel.",
"prompt.toast.worktreeCreateFailed.title": "Kunne ikke opprette worktree",
"prompt.toast.sessionCreateFailed.title": "Kunne ikke opprette sesjon",
"prompt.toast.shellSendFailed.title": "Kunne ikke sende shell-kommando",
"prompt.toast.commandSendFailed.title": "Kunne ikke sende kommando",
"prompt.toast.promptSendFailed.title": "Kunne ikke sende forespørsel",
"dialog.mcp.title": "MCP-er",
"dialog.mcp.description": "{{enabled}} av {{total}} aktivert",
"dialog.mcp.empty": "Ingen MCP-er konfigurert",
"dialog.lsp.empty": "LSP-er automatisk oppdaget fra filtyper",
"dialog.plugins.empty": "Plugins konfigurert i opencode.json",
"mcp.status.connected": "tilkoblet",
"mcp.status.failed": "mislyktes",
"mcp.status.needs_auth": "trenger autentisering",
"mcp.status.disabled": "deaktivert",
"dialog.fork.empty": "Ingen meldinger å forgrene fra",
"dialog.directory.search.placeholder": "Søk etter mapper",
"dialog.directory.empty": "Ingen mapper funnet",
"dialog.server.title": "Servere",
"dialog.server.description": "Bytt hvilken OpenCode-server denne appen kobler til.",
"dialog.server.search.placeholder": "Søk etter servere",
"dialog.server.empty": "Ingen servere ennå",
"dialog.server.add.title": "Legg til en server",
"dialog.server.add.url": "Server-URL",
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "Kunne ikke koble til server",
"dialog.server.add.checking": "Sjekker...",
"dialog.server.add.button": "Legg til server",
"dialog.server.default.title": "Standardserver",
"dialog.server.default.description":
"Koble til denne serveren ved oppstart i stedet for å starte en lokal server. Krever omstart.",
"dialog.server.default.none": "Ingen server valgt",
"dialog.server.default.set": "Sett gjeldende server som standard",
"dialog.server.default.clear": "Tøm",
"dialog.server.action.remove": "Fjern server",
"dialog.server.menu.edit": "Rediger",
"dialog.server.menu.default": "Sett som standard",
"dialog.server.menu.defaultRemove": "Fjern standard",
"dialog.server.menu.delete": "Slett",
"dialog.server.current": "Gjeldende server",
"dialog.server.status.default": "Standard",
"dialog.project.edit.title": "Rediger prosjekt",
"dialog.project.edit.name": "Navn",
"dialog.project.edit.icon": "Ikon",
"dialog.project.edit.icon.alt": "Prosjektikon",
"dialog.project.edit.icon.hint": "Klikk eller dra et bilde",
"dialog.project.edit.icon.recommended": "Anbefalt: 128x128px",
"dialog.project.edit.color": "Farge",
"dialog.project.edit.color.select": "Velg fargen {{color}}",
"context.breakdown.title": "Kontekstfordeling",
"context.breakdown.note": 'Omtrentlig fordeling av input-tokens. "Annet" inkluderer verktøydefinisjoner og overhead.',
"context.breakdown.system": "System",
"context.breakdown.user": "Bruker",
"context.breakdown.assistant": "Assistent",
"context.breakdown.tool": "Verktøykall",
"context.breakdown.other": "Annet",
"context.systemPrompt.title": "Systemprompt",
"context.rawMessages.title": "Rå meldinger",
"context.stats.session": "Sesjon",
"context.stats.messages": "Meldinger",
"context.stats.provider": "Leverandør",
"context.stats.model": "Modell",
"context.stats.limit": "Kontekstgrense",
"context.stats.totalTokens": "Totalt antall tokens",
"context.stats.usage": "Forbruk",
"context.stats.inputTokens": "Input-tokens",
"context.stats.outputTokens": "Output-tokens",
"context.stats.reasoningTokens": "Resonnerings-tokens",
"context.stats.cacheTokens": "Cache-tokens (les/skriv)",
"context.stats.userMessages": "Brukermeldinger",
"context.stats.assistantMessages": "Assistentmeldinger",
"context.stats.totalCost": "Total kostnad",
"context.stats.sessionCreated": "Sesjon opprettet",
"context.stats.lastActivity": "Siste aktivitet",
"context.usage.tokens": "Tokens",
"context.usage.usage": "Forbruk",
"context.usage.cost": "Kostnad",
"context.usage.clickToView": "Klikk for å se kontekst",
"context.usage.view": "Se kontekstforbruk",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"toast.language.title": "Språk",
"toast.language.description": "Byttet til {{language}}",
"toast.theme.title": "Tema byttet",
"toast.scheme.title": "Fargevalg",
"toast.permissions.autoaccept.on.title": "Godtar endringer automatisk",
"toast.permissions.autoaccept.on.description": "Redigerings- og skrivetillatelser vil bli godkjent automatisk",
"toast.permissions.autoaccept.off.title": "Sluttet å godta endringer automatisk",
"toast.permissions.autoaccept.off.description": "Redigerings- og skrivetillatelser vil kreve godkjenning",
"toast.model.none.title": "Ingen modell valgt",
"toast.model.none.description": "Koble til en leverandør for å oppsummere denne sesjonen",
"toast.file.loadFailed.title": "Kunne ikke laste fil",
"toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til utklippstavlen",
"toast.session.share.success.title": "Sesjon delt",
"toast.session.share.success.description": "Delings-URL kopiert til utklippstavlen!",
"toast.session.share.failed.title": "Kunne ikke dele sesjon",
"toast.session.share.failed.description": "Det oppstod en feil under deling av sesjonen",
"toast.session.unshare.success.title": "Deling av sesjon stoppet",
"toast.session.unshare.success.description": "Sesjonen deles ikke lenger!",
"toast.session.unshare.failed.title": "Kunne ikke stoppe deling av sesjon",
"toast.session.unshare.failed.description": "Det oppstod en feil da delingen av sesjonen skulle stoppes",
"toast.session.listFailed.title": "Kunne ikke laste sesjoner for {{project}}",
"toast.update.title": "Oppdatering tilgjengelig",
"toast.update.description": "En ny versjon av OpenCode ({{version}}) er nå tilgjengelig for installasjon.",
"toast.update.action.installRestart": "Installer og start på nytt",
"toast.update.action.notYet": "Ikke nå",
"error.page.title": "Noe gikk galt",
"error.page.description": "Det oppstod en feil under lasting av applikasjonen.",
"error.page.details.label": "Feildetaljer",
"error.page.action.restart": "Start på nytt",
"error.page.action.checking": "Sjekker...",
"error.page.action.checkUpdates": "Se etter oppdateringer",
"error.page.action.updateTo": "Oppdater til {{version}}",
"error.page.report.prefix": "Vennligst rapporter denne feilen til OpenCode-teamet",
"error.page.report.discord": "på Discord",
"error.page.version": "Versjon: {{version}}",
"error.dev.rootNotFound":
"Rotelement ikke funnet. Glemte du å legge det til i index.html? Eller kanskje id-attributten er feilstavet?",
"error.globalSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?",
"error.chain.unknown": "Ukjent feil",
"error.chain.causedBy": "Forårsaket av:",
"error.chain.apiError": "API-feil",
"error.chain.status": "Status: {{status}}",
"error.chain.retryable": "Kan prøves på nytt: {{retryable}}",
"error.chain.responseBody": "Responsinnhold:\n{{body}}",
"error.chain.didYouMean": "Mente du: {{suggestions}}",
"error.chain.modelNotFound": "Modell ikke funnet: {{provider}}/{{model}}",
"error.chain.checkConfig": "Sjekk leverandør-/modellnavnene i konfigurasjonen din (opencode.json)",
"error.chain.mcpFailed": 'MCP-server "{{name}}" mislyktes. Merk at OpenCode ikke støtter MCP-autentisering ennå.',
"error.chain.providerAuthFailed": "Leverandørautentisering mislyktes ({{provider}}): {{message}}",
"error.chain.providerInitFailed":
'Kunne ikke initialisere leverandør "{{provider}}". Sjekk legitimasjon og konfigurasjon.',
"error.chain.configJsonInvalid": "Konfigurasjonsfilen på {{path}} er ikke gyldig JSON(C)",
"error.chain.configJsonInvalidWithMessage": "Konfigurasjonsfilen på {{path}} er ikke gyldig JSON(C): {{message}}",
"error.chain.configDirectoryTypo":
'Mappen "{{dir}}" i {{path}} er ikke gyldig. Gi mappen nytt navn til "{{suggestion}}" eller fjern den. Dette er en vanlig skrivefeil.',
"error.chain.configFrontmatterError": "Kunne ikke analysere frontmatter i {{path}}:\n{{message}}",
"error.chain.configInvalid": "Konfigurasjonsfilen på {{path}} er ugyldig",
"error.chain.configInvalidWithMessage": "Konfigurasjonsfilen på {{path}} er ugyldig: {{message}}",
"notification.permission.title": "Tillatelse påkrevd",
"notification.permission.description": "{{sessionTitle}} i {{projectName}} trenger tillatelse",
"notification.question.title": "Spørsmål",
"notification.question.description": "{{sessionTitle}} i {{projectName}} har et spørsmål",
"notification.action.goToSession": "Gå til sesjon",
"notification.session.responseReady.title": "Svar klart",
"notification.session.error.title": "Sesjonsfeil",
"notification.session.error.fallbackDescription": "Det oppstod en feil",
"home.recentProjects": "Nylige prosjekter",
"home.empty.title": "Ingen nylige prosjekter",
"home.empty.description": "Kom i gang ved å åpne et lokalt prosjekt",
"session.tab.session": "Sesjon",
"session.tab.review": "Gjennomgang",
"session.tab.context": "Kontekst",
"session.panel.reviewAndFiles": "Gjennomgang og filer",
"session.review.filesChanged": "{{count}} filer endret",
"session.review.loadingChanges": "Laster endringer...",
"session.review.empty": "Ingen endringer i denne sesjonen ennå",
"session.messages.renderEarlier": "Vis tidligere meldinger",
"session.messages.loadingEarlier": "Laster inn tidligere meldinger...",
"session.messages.loadEarlier": "Last inn tidligere meldinger",
"session.messages.loading": "Laster meldinger...",
"session.messages.jumpToLatest": "Hopp til nyeste",
"session.context.addToContext": "Legg til {{selection}} i kontekst",
"session.new.worktree.main": "Hovedgren",
"session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})",
"session.new.worktree.create": "Opprett nytt worktree",
"session.new.lastModified": "Sist endret",
"session.header.search.placeholder": "Søk i {{project}}",
"session.header.searchFiles": "Søk etter filer",
"status.popover.trigger": "Status",
"status.popover.ariaLabel": "Serverkonfigurasjoner",
"status.popover.tab.servers": "Servere",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Administrer servere",
"session.share.popover.title": "Publiser på nett",
"session.share.popover.description.shared":
"Denne sesjonen er offentlig på nettet. Den er tilgjengelig for alle med lenken.",
"session.share.popover.description.unshared":
"Del sesjonen offentlig på nettet. Den vil være tilgjengelig for alle med lenken.",
"session.share.action.share": "Del",
"session.share.action.publish": "Publiser",
"session.share.action.publishing": "Publiserer...",
"session.share.action.unpublish": "Avpubliser",
"session.share.action.unpublishing": "Avpubliserer...",
"session.share.action.view": "Vis",
"session.share.copy.copied": "Kopiert",
"session.share.copy.copyLink": "Kopier lenke",
"lsp.tooltip.none": "Ingen LSP-servere",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Laster prompt...",
"terminal.loading": "Laster terminal...",
"terminal.title": "Terminal",
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Lukk terminal",
"terminal.connectionLost.title": "Tilkobling mistet",
"terminal.connectionLost.description":
"Terminalforbindelsen ble avbrutt. Dette kan skje når serveren starter på nytt.",
"common.closeTab": "Lukk fane",
"common.dismiss": "Avvis",
"common.requestFailed": "Forespørsel mislyktes",
"common.moreOptions": "Flere alternativer",
"common.learnMore": "Lær mer",
"common.rename": "Gi nytt navn",
"common.reset": "Tilbakestill",
"common.delete": "Slett",
"common.close": "Lukk",
"common.edit": "Rediger",
"common.loadMore": "Last flere",
"common.key.esc": "ESC",
"sidebar.menu.toggle": "Veksle meny",
"sidebar.nav.projectsAndSessions": "Prosjekter og sesjoner",
"sidebar.settings": "Innstillinger",
"sidebar.help": "Hjelp",
"sidebar.workspaces.enable": "Aktiver arbeidsområder",
"sidebar.workspaces.disable": "Deaktiver arbeidsområder",
"sidebar.gettingStarted.title": "Kom i gang",
"sidebar.gettingStarted.line1": "OpenCode inkluderer gratis modeller så du kan starte umiddelbart.",
"sidebar.gettingStarted.line2": "Koble til en leverandør for å bruke modeller, inkl. Claude, GPT, Gemini osv.",
"sidebar.project.recentSessions": "Nylige sesjoner",
"sidebar.project.viewAllSessions": "Vis alle sesjoner",
"settings.section.desktop": "Skrivebord",
"settings.tab.general": "Generelt",
"settings.tab.shortcuts": "Snarveier",
"settings.general.section.appearance": "Utseende",
"settings.general.section.notifications": "Systemvarsler",
"settings.general.section.sounds": "Lydeffekter",
"settings.general.row.language.title": "Språk",
"settings.general.row.language.description": "Endre visningsspråket for OpenCode",
"settings.general.row.appearance.title": "Utseende",
"settings.general.row.appearance.description": "Tilpass hvordan OpenCode ser ut på enheten din",
"settings.general.row.theme.title": "Tema",
"settings.general.row.theme.description": "Tilpass hvordan OpenCode er tematisert.",
"settings.general.row.font.title": "Skrift",
"settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker",
"settings.general.notifications.agent.title": "Agent",
"settings.general.notifications.agent.description":
"Vis systemvarsel når agenten er ferdig eller trenger oppmerksomhet",
"settings.general.notifications.permissions.title": "Tillatelser",
"settings.general.notifications.permissions.description": "Vis systemvarsel når en tillatelse er påkrevd",
"settings.general.notifications.errors.title": "Feil",
"settings.general.notifications.errors.description": "Vis systemvarsel når det oppstår en feil",
"settings.general.sounds.agent.title": "Agent",
"settings.general.sounds.agent.description": "Spill av lyd når agenten er ferdig eller trenger oppmerksomhet",
"settings.general.sounds.permissions.title": "Tillatelser",
"settings.general.sounds.permissions.description": "Spill av lyd når en tillatelse er påkrevd",
"settings.general.sounds.errors.title": "Feil",
"settings.general.sounds.errors.description": "Spill av lyd når det oppstår en feil",
"settings.shortcuts.title": "Tastatursnarveier",
"settings.shortcuts.reset.button": "Tilbakestill til standard",
"settings.shortcuts.reset.toast.title": "Snarveier tilbakestilt",
"settings.shortcuts.reset.toast.description": "Tastatursnarveier er tilbakestilt til standard.",
"settings.shortcuts.conflict.title": "Snarvei allerede i bruk",
"settings.shortcuts.conflict.description": "{{keybind}} er allerede tilordnet til {{titles}}.",
"settings.shortcuts.unassigned": "Ikke tilordnet",
"settings.shortcuts.pressKeys": "Trykk taster",
"settings.shortcuts.search.placeholder": "Søk etter snarveier",
"settings.shortcuts.search.empty": "Ingen snarveier funnet",
"settings.shortcuts.group.general": "Generelt",
"settings.shortcuts.group.session": "Sesjon",
"settings.shortcuts.group.navigation": "Navigasjon",
"settings.shortcuts.group.modelAndAgent": "Modell og agent",
"settings.shortcuts.group.terminal": "Terminal",
"settings.shortcuts.group.prompt": "Prompt",
"settings.providers.title": "Leverandører",
"settings.providers.description": "Leverandørinnstillinger vil kunne konfigureres her.",
"settings.models.title": "Modeller",
"settings.models.description": "Modellinnstillinger vil kunne konfigureres her.",
"settings.agents.title": "Agenter",
"settings.agents.description": "Agentinnstillinger vil kunne konfigureres her.",
"settings.commands.title": "Kommandoer",
"settings.commands.description": "Kommandoinnstillinger vil kunne konfigureres her.",
"settings.mcp.title": "MCP",
"settings.mcp.description": "MCP-innstillinger vil kunne konfigureres her.",
"settings.permissions.title": "Tillatelser",
"settings.permissions.description": "Kontroller hvilke verktøy serveren kan bruke som standard.",
"settings.permissions.section.tools": "Verktøy",
"settings.permissions.toast.updateFailed.title": "Kunne ikke oppdatere tillatelser",
"settings.permissions.action.allow": "Tillat",
"settings.permissions.action.ask": "Spør",
"settings.permissions.action.deny": "Avslå",
"settings.permissions.tool.read.title": "Les",
"settings.permissions.tool.read.description": "Lesing av en fil (matcher filbanen)",
"settings.permissions.tool.edit.title": "Rediger",
"settings.permissions.tool.edit.description":
"Endre filer, inkludert redigeringer, skriving, patcher og multi-redigeringer",
"settings.permissions.tool.glob.title": "Glob",
"settings.permissions.tool.glob.description": "Match filer ved hjelp av glob-mønstre",
"settings.permissions.tool.grep.title": "Grep",
"settings.permissions.tool.grep.description": "Søk i filinnhold ved hjelp av regulære uttrykk",
"settings.permissions.tool.list.title": "Liste",
"settings.permissions.tool.list.description": "List filer i en mappe",
"settings.permissions.tool.bash.title": "Bash",
"settings.permissions.tool.bash.description": "Kjør shell-kommandoer",
"settings.permissions.tool.task.title": "Oppgave",
"settings.permissions.tool.task.description": "Start underagenter",
"settings.permissions.tool.skill.title": "Ferdighet",
"settings.permissions.tool.skill.description": "Last en ferdighet etter navn",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Kjør språkserverforespørsler",
"settings.permissions.tool.todoread.title": "Les gjøremål",
"settings.permissions.tool.todoread.description": "Les gjøremålslisten",
"settings.permissions.tool.todowrite.title": "Skriv gjøremål",
"settings.permissions.tool.todowrite.description": "Oppdater gjøremålslisten",
"settings.permissions.tool.webfetch.title": "Webhenting",
"settings.permissions.tool.webfetch.description": "Hent innhold fra en URL",
"settings.permissions.tool.websearch.title": "Websøk",
"settings.permissions.tool.websearch.description": "Søk på nettet",
"settings.permissions.tool.codesearch.title": "Kodesøk",
"settings.permissions.tool.codesearch.description": "Søk etter kode på nettet",
"settings.permissions.tool.external_directory.title": "Ekstern mappe",
"settings.permissions.tool.external_directory.description": "Få tilgang til filer utenfor prosjektmappen",
"settings.permissions.tool.doom_loop.title": "Doom Loop",
"settings.permissions.tool.doom_loop.description": "Oppdager gjentatte verktøykall med identisk input",
"workspace.new": "Nytt arbeidsområde",
"workspace.type.local": "lokal",
"workspace.type.sandbox": "sandkasse",
"workspace.create.failed.title": "Kunne ikke opprette arbeidsområde",
"workspace.delete.failed.title": "Kunne ikke slette arbeidsområde",
"workspace.resetting.title": "Tilbakestiller arbeidsområde",
"workspace.resetting.description": "Dette kan ta et minutt.",
"workspace.reset.failed.title": "Kunne ikke tilbakestille arbeidsområde",
"workspace.reset.success.title": "Arbeidsområde tilbakestilt",
"workspace.reset.success.description": "Arbeidsområdet samsvarer nå med standardgrenen.",
"workspace.status.checking": "Sjekker for ikke-sammenslåtte endringer...",
"workspace.status.error": "Kunne ikke bekrefte git-status.",
"workspace.status.clean": "Ingen ikke-sammenslåtte endringer oppdaget.",
"workspace.status.dirty": "Ikke-sammenslåtte endringer oppdaget i dette arbeidsområdet.",
"workspace.delete.title": "Slett arbeidsområde",
"workspace.delete.confirm": 'Slette arbeidsområdet "{{name}}"?',
"workspace.delete.button": "Slett arbeidsområde",
"workspace.reset.title": "Tilbakestill arbeidsområde",
"workspace.reset.confirm": 'Tilbakestille arbeidsområdet "{{name}}"?',
"workspace.reset.button": "Tilbakestill arbeidsområde",
"workspace.reset.archived.none": "Ingen aktive sesjoner vil bli arkivert.",
"workspace.reset.archived.one": "1 sesjon vil bli arkivert.",
"workspace.reset.archived.many": "{{count}} sesjoner vil bli arkivert.",
"workspace.reset.note": "Dette vil tilbakestille arbeidsområdet til å samsvare med standardgrenen.",
} satisfies Partial<Record<Keys, string>>

680
packages/app/src/i18n/pl.ts Normal file
View File

@@ -0,0 +1,680 @@
export const dict = {
"command.category.suggested": "Sugerowane",
"command.category.view": "Widok",
"command.category.project": "Projekt",
"command.category.provider": "Dostawca",
"command.category.server": "Serwer",
"command.category.session": "Sesja",
"command.category.theme": "Motyw",
"command.category.language": "Język",
"command.category.file": "Plik",
"command.category.terminal": "Terminal",
"command.category.model": "Model",
"command.category.mcp": "MCP",
"command.category.agent": "Agent",
"command.category.permissions": "Uprawnienia",
"command.category.workspace": "Przestrzeń robocza",
"command.category.settings": "Ustawienia",
"theme.scheme.system": "Systemowy",
"theme.scheme.light": "Jasny",
"theme.scheme.dark": "Ciemny",
"command.sidebar.toggle": "Przełącz pasek boczny",
"command.project.open": "Otwórz projekt",
"command.provider.connect": "Połącz dostawcę",
"command.server.switch": "Przełącz serwer",
"command.settings.open": "Otwórz ustawienia",
"command.session.previous": "Poprzednia sesja",
"command.session.next": "Następna sesja",
"command.session.archive": "Zarchiwizuj sesję",
"command.palette": "Paleta poleceń",
"command.theme.cycle": "Przełącz motyw",
"command.theme.set": "Użyj motywu: {{theme}}",
"command.theme.scheme.cycle": "Przełącz schemat kolorów",
"command.theme.scheme.set": "Użyj schematu kolorów: {{scheme}}",
"command.language.cycle": "Przełącz język",
"command.language.set": "Użyj języka: {{language}}",
"command.session.new": "Nowa sesja",
"command.file.open": "Otwórz plik",
"command.file.open.description": "Szukaj plików i poleceń",
"command.terminal.toggle": "Przełącz terminal",
"command.review.toggle": "Przełącz przegląd",
"command.terminal.new": "Nowy terminal",
"command.terminal.new.description": "Utwórz nową kartę terminala",
"command.steps.toggle": "Przełącz kroki",
"command.steps.toggle.description": "Pokaż lub ukryj kroki dla bieżącej wiadomości",
"command.message.previous": "Poprzednia wiadomość",
"command.message.previous.description": "Przejdź do poprzedniej wiadomości użytkownika",
"command.message.next": "Następna wiadomość",
"command.message.next.description": "Przejdź do następnej wiadomości użytkownika",
"command.model.choose": "Wybierz model",
"command.model.choose.description": "Wybierz inny model",
"command.mcp.toggle": "Przełącz MCP",
"command.mcp.toggle.description": "Przełącz MCP",
"command.agent.cycle": "Przełącz agenta",
"command.agent.cycle.description": "Przełącz na następnego agenta",
"command.agent.cycle.reverse": "Przełącz agenta wstecz",
"command.agent.cycle.reverse.description": "Przełącz na poprzedniego agenta",
"command.model.variant.cycle": "Przełącz wysiłek myślowy",
"command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku",
"command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji",
"command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie edycji",
"command.session.undo": "Cofnij",
"command.session.undo.description": "Cofnij ostatnią wiadomość",
"command.session.redo": "Ponów",
"command.session.redo.description": "Ponów ostatnią cofniętą wiadomość",
"command.session.compact": "Kompaktuj sesję",
"command.session.compact.description": "Podsumuj sesję, aby zmniejszyć rozmiar kontekstu",
"command.session.fork": "Rozwidlij od wiadomości",
"command.session.fork.description": "Utwórz nową sesję od poprzedniej wiadomości",
"command.session.share": "Udostępnij sesję",
"command.session.share.description": "Udostępnij tę sesję i skopiuj URL do schowka",
"command.session.unshare": "Przestań udostępniać sesję",
"command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji",
"palette.search.placeholder": "Szukaj plików i poleceń",
"palette.empty": "Brak wyników",
"palette.group.commands": "Polecenia",
"palette.group.files": "Pliki",
"dialog.provider.search.placeholder": "Szukaj dostawców",
"dialog.provider.empty": "Nie znaleziono dostawców",
"dialog.provider.group.popular": "Popularne",
"dialog.provider.group.other": "Inne",
"dialog.provider.tag.recommended": "Zalecane",
"dialog.provider.anthropic.note": "Połącz z Claude Pro/Max lub kluczem API",
"dialog.provider.openai.note": "Połącz z ChatGPT Pro/Plus lub kluczem API",
"dialog.provider.copilot.note": "Połącz z Copilot lub kluczem API",
"dialog.model.select.title": "Wybierz model",
"dialog.model.search.placeholder": "Szukaj modeli",
"dialog.model.empty": "Brak wyników modelu",
"dialog.model.manage": "Zarządzaj modelami",
"dialog.model.manage.description": "Dostosuj, które modele pojawiają się w wyborze modelu.",
"dialog.model.unpaid.freeModels.title": "Darmowe modele dostarczane przez OpenCode",
"dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców",
"dialog.provider.viewAll": "Zobacz więcej dostawców",
"provider.connect.title": "Połącz {{provider}}",
"provider.connect.title.anthropicProMax": "Zaloguj się z Claude Pro/Max",
"provider.connect.selectMethod": "Wybierz metodę logowania dla {{provider}}.",
"provider.connect.method.apiKey": "Klucz API",
"provider.connect.status.inProgress": "Autoryzacja w toku...",
"provider.connect.status.waiting": "Oczekiwanie na autoryzację...",
"provider.connect.status.failed": "Autoryzacja nie powiodła się: {{error}}",
"provider.connect.apiKey.description":
"Wprowadź swój klucz API {{provider}}, aby połączyć konto i używać modeli {{provider}} w OpenCode.",
"provider.connect.apiKey.label": "Klucz API {{provider}}",
"provider.connect.apiKey.placeholder": "Klucz API",
"provider.connect.apiKey.required": "Klucz API jest wymagany",
"provider.connect.opencodeZen.line1":
"OpenCode Zen daje dostęp do wybranego zestawu niezawodnych, zoptymalizowanych modeli dla agentów kodujących.",
"provider.connect.opencodeZen.line2":
"Z jednym kluczem API uzyskasz dostęp do modeli takich jak Claude, GPT, Gemini, GLM i więcej.",
"provider.connect.opencodeZen.visit.prefix": "Odwiedź ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": ", aby odebrać swój klucz API.",
"provider.connect.oauth.code.visit.prefix": "Odwiedź ",
"provider.connect.oauth.code.visit.link": "ten link",
"provider.connect.oauth.code.visit.suffix":
", aby odebrać kod autoryzacyjny, połączyć konto i używać modeli {{provider}} w OpenCode.",
"provider.connect.oauth.code.label": "Kod autoryzacyjny {{method}}",
"provider.connect.oauth.code.placeholder": "Kod autoryzacyjny",
"provider.connect.oauth.code.required": "Kod autoryzacyjny jest wymagany",
"provider.connect.oauth.code.invalid": "Nieprawidłowy kod autoryzacyjny",
"provider.connect.oauth.auto.visit.prefix": "Odwiedź ",
"provider.connect.oauth.auto.visit.link": "ten link",
"provider.connect.oauth.auto.visit.suffix":
" i wprowadź poniższy kod, aby połączyć konto i używać modeli {{provider}} w OpenCode.",
"provider.connect.oauth.auto.confirmationCode": "Kod potwierdzający",
"provider.connect.toast.connected.title": "Połączono {{provider}}",
"provider.connect.toast.connected.description": "Modele {{provider}} są teraz dostępne do użycia.",
"model.tag.free": "Darmowy",
"model.tag.latest": "Najnowszy",
"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": "obraz",
"model.input.audio": "audio",
"model.input.video": "wideo",
"model.input.pdf": "pdf",
"model.tooltip.allows": "Obsługuje: {{inputs}}",
"model.tooltip.reasoning.allowed": "Obsługuje wnioskowanie",
"model.tooltip.reasoning.none": "Brak wnioskowania",
"model.tooltip.context": "Limit kontekstu {{limit}}",
"common.search.placeholder": "Szukaj",
"common.goBack": "Wstecz",
"common.loading": "Ładowanie",
"common.loading.ellipsis": "...",
"common.cancel": "Anuluj",
"common.submit": "Prześlij",
"common.save": "Zapisz",
"common.saving": "Zapisywanie...",
"common.default": "Domyślny",
"common.attachment": "załącznik",
"prompt.placeholder.shell": "Wpisz polecenie terminala...",
"prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"',
"prompt.mode.shell": "Terminal",
"prompt.mode.shell.exit": "esc aby wyjść",
"prompt.example.1": "Napraw TODO w bazie kodu",
"prompt.example.2": "Jaki jest stos technologiczny tego projektu?",
"prompt.example.3": "Napraw zepsute testy",
"prompt.example.4": "Wyjaśnij jak działa uwierzytelnianie",
"prompt.example.5": "Znajdź i napraw luki w zabezpieczeniach",
"prompt.example.6": "Dodaj testy jednostkowe dla serwisu użytkownika",
"prompt.example.7": "Zrefaktoryzuj tę funkcję, aby była bardziej czytelna",
"prompt.example.8": "Co oznacza ten błąd?",
"prompt.example.9": "Pomóż mi zdebugować ten problem",
"prompt.example.10": "Wygeneruj dokumentację API",
"prompt.example.11": "Zoptymalizuj zapytania do bazy danych",
"prompt.example.12": "Dodaj walidację danych wejściowych",
"prompt.example.13": "Utwórz nowy komponent dla...",
"prompt.example.14": "Jak wdrożyć ten projekt?",
"prompt.example.15": "Sprawdź mój kod pod kątem najlepszych praktyk",
"prompt.example.16": "Dodaj obsługę błędów do tej funkcję",
"prompt.example.17": "Wyjaśnij ten wzorzec regex",
"prompt.example.18": "Przekonwertuj to na TypeScript",
"prompt.example.19": "Dodaj logowanie w całej bazie kodu",
"prompt.example.20": "Które zależności są przestarzałe?",
"prompt.example.21": "Pomóż mi napisać skrypt migracyjny",
"prompt.example.22": "Zaimplementuj cachowanie dla tego punktu końcowego",
"prompt.example.23": "Dodaj stronicowanie do tej listy",
"prompt.example.24": "Utwórz polecenie CLI dla...",
"prompt.example.25": "Jak działają tutaj zmienne środowiskowe?",
"prompt.popover.emptyResults": "Brak pasujących wyników",
"prompt.popover.emptyCommands": "Brak pasujących poleceń",
"prompt.dropzone.label": "Upuść obrazy lub pliki PDF tutaj",
"prompt.slash.badge.custom": "własne",
"prompt.context.active": "aktywny",
"prompt.context.includeActiveFile": "Dołącz aktywny plik",
"prompt.context.removeActiveFile": "Usuń aktywny plik z kontekstu",
"prompt.context.removeFile": "Usuń plik z kontekstu",
"prompt.action.attachFile": "Załącz plik",
"prompt.attachment.remove": "Usuń załącznik",
"prompt.action.send": "Wyślij",
"prompt.action.stop": "Zatrzymaj",
"prompt.toast.pasteUnsupported.title": "Nieobsługiwane wklejanie",
"prompt.toast.pasteUnsupported.description": "Tylko obrazy lub pliki PDF mogą być tutaj wklejane.",
"prompt.toast.modelAgentRequired.title": "Wybierz agenta i model",
"prompt.toast.modelAgentRequired.description": "Wybierz agenta i model przed wysłaniem zapytania.",
"prompt.toast.worktreeCreateFailed.title": "Nie udało się utworzyć drzewa roboczego",
"prompt.toast.sessionCreateFailed.title": "Nie udało się utworzyć sesji",
"prompt.toast.shellSendFailed.title": "Nie udało się wysłać polecenia powłoki",
"prompt.toast.commandSendFailed.title": "Nie udało się wysłać polecenia",
"prompt.toast.promptSendFailed.title": "Nie udało się wysłać zapytania",
"dialog.mcp.title": "MCP",
"dialog.mcp.description": "{{enabled}} z {{total}} włączone",
"dialog.mcp.empty": "Brak skonfigurowanych MCP",
"dialog.lsp.empty": "LSP wykryte automatycznie na podstawie typów plików",
"dialog.plugins.empty": "Wtyczki skonfigurowane w opencode.json",
"mcp.status.connected": "połączono",
"mcp.status.failed": "niepowodzenie",
"mcp.status.needs_auth": "wymaga autoryzacji",
"mcp.status.disabled": "wyłączone",
"dialog.fork.empty": "Brak wiadomości do rozwidlenia",
"dialog.directory.search.placeholder": "Szukaj folderów",
"dialog.directory.empty": "Nie znaleziono folderów",
"dialog.server.title": "Serwery",
"dialog.server.description": "Przełącz serwer OpenCode, z którym łączy się ta aplikacja.",
"dialog.server.search.placeholder": "Szukaj serwerów",
"dialog.server.empty": "Brak serwerów",
"dialog.server.add.title": "Dodaj serwer",
"dialog.server.add.url": "URL serwera",
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "Nie można połączyć się z serwerem",
"dialog.server.add.checking": "Sprawdzanie...",
"dialog.server.add.button": "Dodaj serwer",
"dialog.server.default.title": "Domyślny serwer",
"dialog.server.default.description":
"Połącz z tym serwerem przy uruchomieniu aplikacji zamiast uruchamiać lokalny serwer. Wymaga restartu.",
"dialog.server.default.none": "Nie wybrano serwera",
"dialog.server.default.set": "Ustaw bieżący serwer jako domyślny",
"dialog.server.default.clear": "Wyczyść",
"dialog.server.action.remove": "Usuń serwer",
"dialog.server.menu.edit": "Edytuj",
"dialog.server.menu.default": "Ustaw jako domyślny",
"dialog.server.menu.defaultRemove": "Usuń domyślny",
"dialog.server.menu.delete": "Usuń",
"dialog.server.current": "Obecny serwer",
"dialog.server.status.default": "Domyślny",
"dialog.project.edit.title": "Edytuj projekt",
"dialog.project.edit.name": "Nazwa",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Ikona projektu",
"dialog.project.edit.icon.hint": "Kliknij lub przeciągnij obraz",
"dialog.project.edit.icon.recommended": "Zalecane: 128x128px",
"dialog.project.edit.color": "Kolor",
"dialog.project.edit.color.select": "Wybierz kolor {{color}}",
"context.breakdown.title": "Podział kontekstu",
"context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.',
"context.breakdown.system": "System",
"context.breakdown.user": "Użytkownik",
"context.breakdown.assistant": "Asystent",
"context.breakdown.tool": "Wywołania narzędzi",
"context.breakdown.other": "Inne",
"context.systemPrompt.title": "Prompt systemowy",
"context.rawMessages.title": "Surowe wiadomości",
"context.stats.session": "Sesja",
"context.stats.messages": "Wiadomości",
"context.stats.provider": "Dostawca",
"context.stats.model": "Model",
"context.stats.limit": "Limit kontekstu",
"context.stats.totalTokens": "Całkowita liczba tokenów",
"context.stats.usage": "Użycie",
"context.stats.inputTokens": "Tokeny wejściowe",
"context.stats.outputTokens": "Tokeny wyjściowe",
"context.stats.reasoningTokens": "Tokeny wnioskowania",
"context.stats.cacheTokens": "Tokeny pamięci podręcznej (odczyt/zapis)",
"context.stats.userMessages": "Wiadomości użytkownika",
"context.stats.assistantMessages": "Wiadomości asystenta",
"context.stats.totalCost": "Całkowity koszt",
"context.stats.sessionCreated": "Utworzono sesję",
"context.stats.lastActivity": "Ostatnia aktywność",
"context.usage.tokens": "Tokeny",
"context.usage.usage": "Użycie",
"context.usage.cost": "Koszt",
"context.usage.clickToView": "Kliknij, aby zobaczyć kontekst",
"context.usage.view": "Pokaż użycie kontekstu",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"toast.language.title": "Język",
"toast.language.description": "Przełączono na {{language}}",
"toast.theme.title": "Przełączono motyw",
"toast.scheme.title": "Schemat kolorów",
"toast.permissions.autoaccept.on.title": "Automatyczne akceptowanie edycji",
"toast.permissions.autoaccept.on.description": "Uprawnienia do edycji i zapisu będą automatycznie zatwierdzane",
"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.model.none.title": "Nie wybrano modelu",
"toast.model.none.description": "Połącz dostawcę, aby podsumować tę sesję",
"toast.file.loadFailed.title": "Nie udało się załadować pliku",
"toast.session.share.copyFailed.title": "Nie udało się skopiować URL do schowka",
"toast.session.share.success.title": "Sesja udostępniona",
"toast.session.share.success.description": "Link udostępniania skopiowany do schowka!",
"toast.session.share.failed.title": "Nie udało się udostępnić sesji",
"toast.session.share.failed.description": "Wystąpił błąd podczas udostępniania sesji",
"toast.session.unshare.success.title": "Zatrzymano udostępnianie sesji",
"toast.session.unshare.success.description": "Udostępnianie sesji zostało pomyślnie zatrzymane!",
"toast.session.unshare.failed.title": "Nie udało się zatrzymać udostępniania sesji",
"toast.session.unshare.failed.description": "Wystąpił błąd podczas zatrzymywania udostępniania sesji",
"toast.session.listFailed.title": "Nie udało się załadować sesji dla {{project}}",
"toast.update.title": "Dostępna aktualizacja",
"toast.update.description": "Nowa wersja OpenCode ({{version}}) jest teraz dostępna do instalacji.",
"toast.update.action.installRestart": "Zainstaluj i zrestartuj",
"toast.update.action.notYet": "Jeszcze nie",
"error.page.title": "Coś poszło nie tak",
"error.page.description": "Wystąpił błąd podczas ładowania aplikacji.",
"error.page.details.label": "Szczegóły błędu",
"error.page.action.restart": "Restartuj",
"error.page.action.checking": "Sprawdzanie...",
"error.page.action.checkUpdates": "Sprawdź aktualizacje",
"error.page.action.updateTo": "Zaktualizuj do {{version}}",
"error.page.report.prefix": "Proszę zgłosić ten błąd do zespołu OpenCode",
"error.page.report.discord": "na Discordzie",
"error.page.version": "Wersja: {{version}}",
"error.dev.rootNotFound":
"Nie znaleziono elementu głównego. Czy zapomniałeś dodać go do swojego index.html? A może atrybut id został błędnie wpisany?",
"error.globalSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?",
"error.chain.unknown": "Nieznany błąd",
"error.chain.causedBy": "Spowodowany przez:",
"error.chain.apiError": "Błąd API",
"error.chain.status": "Status: {{status}}",
"error.chain.retryable": "Można ponowić: {{retryable}}",
"error.chain.responseBody": "Treść odpowiedzi:\n{{body}}",
"error.chain.didYouMean": "Czy miałeś na myśli: {{suggestions}}",
"error.chain.modelNotFound": "Model nie znaleziony: {{provider}}/{{model}}",
"error.chain.checkConfig": "Sprawdź swoją konfigurację (opencode.json) nazwy dostawców/modeli",
"error.chain.mcpFailed":
'Serwer MCP "{{name}}" nie powiódł się. Uwaga, OpenCode nie obsługuje jeszcze uwierzytelniania MCP.',
"error.chain.providerAuthFailed": "Uwierzytelnianie dostawcy nie powiodło się ({{provider}}): {{message}}",
"error.chain.providerInitFailed":
'Nie udało się zainicjować dostawcy "{{provider}}". Sprawdź poświadczenia i konfigurację.',
"error.chain.configJsonInvalid": "Plik konfiguracyjny w {{path}} nie jest poprawnym JSON(C)",
"error.chain.configJsonInvalidWithMessage": "Plik konfiguracyjny w {{path}} nie jest poprawnym JSON(C): {{message}}",
"error.chain.configDirectoryTypo":
'Katalog "{{dir}}" w {{path}} jest nieprawidłowy. Zmień nazwę katalogu na "{{suggestion}}" lub usuń go. To częsta literówka.',
"error.chain.configFrontmatterError": "Nie udało się przetworzyć frontmatter w {{path}}:\n{{message}}",
"error.chain.configInvalid": "Plik konfiguracyjny w {{path}} jest nieprawidłowy",
"error.chain.configInvalidWithMessage": "Plik konfiguracyjny w {{path}} jest nieprawidłowy: {{message}}",
"notification.permission.title": "Wymagane uprawnienie",
"notification.permission.description": "{{sessionTitle}} w {{projectName}} potrzebuje uprawnienia",
"notification.question.title": "Pytanie",
"notification.question.description": "{{sessionTitle}} w {{projectName}} ma pytanie",
"notification.action.goToSession": "Przejdź do sesji",
"notification.session.responseReady.title": "Odpowiedź gotowa",
"notification.session.error.title": "Błąd sesji",
"notification.session.error.fallbackDescription": "Wystąpił błąd",
"home.recentProjects": "Ostatnie projekty",
"home.empty.title": "Brak ostatnich projektów",
"home.empty.description": "Zacznij od otwarcia lokalnego projektu",
"session.tab.session": "Sesja",
"session.tab.review": "Przegląd",
"session.tab.context": "Kontekst",
"session.panel.reviewAndFiles": "Przegląd i pliki",
"session.review.filesChanged": "Zmieniono {{count}} plików",
"session.review.loadingChanges": "Ładowanie zmian...",
"session.review.empty": "Brak zmian w tej sesji",
"session.messages.renderEarlier": "Renderuj wcześniejsze wiadomości",
"session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...",
"session.messages.loadEarlier": "Załaduj wcześniejsze wiadomości",
"session.messages.loading": "Ładowanie wiadomości...",
"session.messages.jumpToLatest": "Przejdź do najnowszych",
"session.context.addToContext": "Dodaj {{selection}} do kontekstu",
"session.new.worktree.main": "Główna gałąź",
"session.new.worktree.mainWithBranch": "Główna gałąź ({{branch}})",
"session.new.worktree.create": "Utwórz nowe drzewo robocze",
"session.new.lastModified": "Ostatnio zmodyfikowano",
"session.header.search.placeholder": "Szukaj {{project}}",
"session.header.searchFiles": "Szukaj plików",
"status.popover.trigger": "Status",
"status.popover.ariaLabel": "Konfiguracje serwerów",
"status.popover.tab.servers": "Serwery",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Wtyczki",
"status.popover.action.manageServers": "Zarządzaj serwerami",
"session.share.popover.title": "Opublikuj w sieci",
"session.share.popover.description.shared":
"Ta sesja jest publiczna w sieci. Jest dostępna dla każdego, kto posiada link.",
"session.share.popover.description.unshared":
"Udostępnij sesję publicznie w sieci. Będzie dostępna dla każdego, kto posiada link.",
"session.share.action.share": "Udostępnij",
"session.share.action.publish": "Opublikuj",
"session.share.action.publishing": "Publikowanie...",
"session.share.action.unpublish": "Cofnij publikację",
"session.share.action.unpublishing": "Cofanie publikacji...",
"session.share.action.view": "Widok",
"session.share.copy.copied": "Skopiowano",
"session.share.copy.copyLink": "Kopiuj link",
"lsp.tooltip.none": "Brak serwerów LSP",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Ładowanie promptu...",
"terminal.loading": "Ładowanie terminala...",
"terminal.title": "Terminal",
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Zamknij terminal",
"terminal.connectionLost.title": "Utracono połączenie",
"terminal.connectionLost.description":
"Połączenie z terminalem zostało przerwane. Może się to zdarzyć przy restarcie serwera.",
"common.closeTab": "Zamknij kartę",
"common.dismiss": "Odrzuć",
"common.requestFailed": "Żądanie nie powiodło się",
"common.moreOptions": "Więcej opcji",
"common.learnMore": "Dowiedz się więcej",
"common.rename": "Zmień nazwę",
"common.reset": "Resetuj",
"common.archive": "Archiwizuj",
"common.delete": "Usuń",
"common.close": "Zamknij",
"common.edit": "Edytuj",
"common.loadMore": "Załaduj więcej",
"common.key.esc": "ESC",
"sidebar.menu.toggle": "Przełącz menu",
"sidebar.nav.projectsAndSessions": "Projekty i sesje",
"sidebar.settings": "Ustawienia",
"sidebar.help": "Pomoc",
"sidebar.workspaces.enable": "Włącz przestrzenie robocze",
"sidebar.workspaces.disable": "Wyłącz przestrzenie robocze",
"sidebar.gettingStarted.title": "Pierwsze kroki",
"sidebar.gettingStarted.line1": "OpenCode zawiera darmowe modele, więc możesz zacząć od razu.",
"sidebar.gettingStarted.line2": "Połącz dowolnego dostawcę, aby używać modeli, w tym Claude, GPT, Gemini itp.",
"sidebar.project.recentSessions": "Ostatnie sesje",
"sidebar.project.viewAllSessions": "Zobacz wszystkie sesje",
"settings.section.desktop": "Pulpit",
"settings.tab.general": "Ogólne",
"settings.tab.shortcuts": "Skróty",
"settings.general.section.appearance": "Wygląd",
"settings.general.section.notifications": "Powiadomienia systemowe",
"settings.general.section.sounds": "Efekty dźwiękowe",
"settings.general.row.language.title": "Język",
"settings.general.row.language.description": "Zmień język wyświetlania dla OpenCode",
"settings.general.row.appearance.title": "Wygląd",
"settings.general.row.appearance.description": "Dostosuj wygląd OpenCode na swoim urządzeniu",
"settings.general.row.theme.title": "Motyw",
"settings.general.row.theme.description": "Dostosuj motyw OpenCode.",
"settings.general.row.font.title": "Czcionka",
"settings.general.row.font.description": "Dostosuj czcionkę mono używaną w blokach kodu",
"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.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": "Alert 01",
"sound.option.alert02": "Alert 02",
"sound.option.alert03": "Alert 03",
"sound.option.alert04": "Alert 04",
"sound.option.alert05": "Alert 05",
"sound.option.alert06": "Alert 06",
"sound.option.alert07": "Alert 07",
"sound.option.alert08": "Alert 08",
"sound.option.alert09": "Alert 09",
"sound.option.alert10": "Alert 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": "Nope 01",
"sound.option.nope02": "Nope 02",
"sound.option.nope03": "Nope 03",
"sound.option.nope04": "Nope 04",
"sound.option.nope05": "Nope 05",
"sound.option.nope06": "Nope 06",
"sound.option.nope07": "Nope 07",
"sound.option.nope08": "Nope 08",
"sound.option.nope09": "Nope 09",
"sound.option.nope10": "Nope 10",
"sound.option.nope11": "Nope 11",
"sound.option.nope12": "Nope 12",
"sound.option.yup01": "Yup 01",
"sound.option.yup02": "Yup 02",
"sound.option.yup03": "Yup 03",
"sound.option.yup04": "Yup 04",
"sound.option.yup05": "Yup 05",
"sound.option.yup06": "Yup 06",
"settings.general.notifications.agent.title": "Agent",
"settings.general.notifications.agent.description":
"Pokaż powiadomienie systemowe, gdy agent zakończy pracę lub wymaga uwagi",
"settings.general.notifications.permissions.title": "Uprawnienia",
"settings.general.notifications.permissions.description":
"Pokaż powiadomienie systemowe, gdy wymagane jest uprawnienie",
"settings.general.notifications.errors.title": "Błędy",
"settings.general.notifications.errors.description": "Pokaż powiadomienie systemowe, gdy wystąpi błąd",
"settings.general.sounds.agent.title": "Agent",
"settings.general.sounds.agent.description": "Odtwórz dźwięk, gdy agent zakończy pracę lub wymaga uwagi",
"settings.general.sounds.permissions.title": "Uprawnienia",
"settings.general.sounds.permissions.description": "Odtwórz dźwięk, gdy wymagane jest uprawnienie",
"settings.general.sounds.errors.title": "Błędy",
"settings.general.sounds.errors.description": "Odtwórz dźwięk, gdy wystąpi błąd",
"settings.shortcuts.title": "Skróty klawiszowe",
"settings.shortcuts.reset.button": "Przywróć domyślne",
"settings.shortcuts.reset.toast.title": "Zresetowano skróty",
"settings.shortcuts.reset.toast.description": "Skróty klawiszowe zostały przywrócone do ustawień domyślnych.",
"settings.shortcuts.conflict.title": "Skrót już w użyciu",
"settings.shortcuts.conflict.description": "{{keybind}} jest już przypisany do {{titles}}.",
"settings.shortcuts.unassigned": "Nieprzypisany",
"settings.shortcuts.pressKeys": "Naciśnij klawisze",
"settings.shortcuts.search.placeholder": "Szukaj skrótów",
"settings.shortcuts.search.empty": "Nie znaleziono skrótów",
"settings.shortcuts.group.general": "Ogólne",
"settings.shortcuts.group.session": "Sesja",
"settings.shortcuts.group.navigation": "Nawigacja",
"settings.shortcuts.group.modelAndAgent": "Model i agent",
"settings.shortcuts.group.terminal": "Terminal",
"settings.shortcuts.group.prompt": "Prompt",
"settings.providers.title": "Dostawcy",
"settings.providers.description": "Ustawienia dostawców będą tutaj konfigurowalne.",
"settings.models.title": "Modele",
"settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.",
"settings.agents.title": "Agenci",
"settings.agents.description": "Ustawienia agentów będą tutaj konfigurowalne.",
"settings.commands.title": "Polecenia",
"settings.commands.description": "Ustawienia poleceń będą tutaj konfigurowalne.",
"settings.mcp.title": "MCP",
"settings.mcp.description": "Ustawienia MCP będą tutaj konfigurowalne.",
"settings.permissions.title": "Uprawnienia",
"settings.permissions.description": "Kontroluj, jakich narzędzi serwer może używać domyślnie.",
"settings.permissions.section.tools": "Narzędzia",
"settings.permissions.toast.updateFailed.title": "Nie udało się zaktualizować uprawnień",
"settings.permissions.action.allow": "Zezwól",
"settings.permissions.action.ask": "Pytaj",
"settings.permissions.action.deny": "Odmów",
"settings.permissions.tool.read.title": "Odczyt",
"settings.permissions.tool.read.description": "Odczyt pliku (pasuje do ścieżki pliku)",
"settings.permissions.tool.edit.title": "Edycja",
"settings.permissions.tool.edit.description": "Modyfikacja plików, w tym edycje, zapisy, łatki i multi-edycje",
"settings.permissions.tool.glob.title": "Glob",
"settings.permissions.tool.glob.description": "Dopasowywanie plików za pomocą wzorców glob",
"settings.permissions.tool.grep.title": "Grep",
"settings.permissions.tool.grep.description": "Przeszukiwanie zawartości plików za pomocą wyrażeń regularnych",
"settings.permissions.tool.list.title": "Lista",
"settings.permissions.tool.list.description": "Wyświetlanie listy plików w katalogu",
"settings.permissions.tool.bash.title": "Bash",
"settings.permissions.tool.bash.description": "Uruchamianie poleceń powłoki",
"settings.permissions.tool.task.title": "Zadanie",
"settings.permissions.tool.task.description": "Uruchamianie pod-agentów",
"settings.permissions.tool.skill.title": "Umiejętność",
"settings.permissions.tool.skill.description": "Ładowanie umiejętności według nazwy",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Uruchamianie zapytań serwera językowego",
"settings.permissions.tool.todoread.title": "Odczyt Todo",
"settings.permissions.tool.todoread.description": "Odczyt listy zadań",
"settings.permissions.tool.todowrite.title": "Zapis Todo",
"settings.permissions.tool.todowrite.description": "Aktualizacja listy zadań",
"settings.permissions.tool.webfetch.title": "Pobieranie z sieci",
"settings.permissions.tool.webfetch.description": "Pobieranie zawartości z adresu URL",
"settings.permissions.tool.websearch.title": "Wyszukiwanie w sieci",
"settings.permissions.tool.websearch.description": "Przeszukiwanie sieci",
"settings.permissions.tool.codesearch.title": "Wyszukiwanie kodu",
"settings.permissions.tool.codesearch.description": "Przeszukiwanie kodu w sieci",
"settings.permissions.tool.external_directory.title": "Katalog zewnętrzny",
"settings.permissions.tool.external_directory.description": "Dostęp do plików poza katalogiem projektu",
"settings.permissions.tool.doom_loop.title": "Zapętlenie",
"settings.permissions.tool.doom_loop.description": "Wykrywanie powtarzających się wywołań narzędzi (doom loop)",
"session.delete.failed.title": "Nie udało się usunąć sesji",
"session.delete.title": "Usuń sesję",
"session.delete.confirm": 'Usunąć sesję "{{name}}"?',
"session.delete.button": "Usuń sesję",
"workspace.new": "Nowa przestrzeń robocza",
"workspace.type.local": "lokalna",
"workspace.type.sandbox": "piaskownica",
"workspace.create.failed.title": "Nie udało się utworzyć przestrzeni roboczej",
"workspace.delete.failed.title": "Nie udało się usunąć przestrzeni roboczej",
"workspace.resetting.title": "Resetowanie przestrzeni roboczej",
"workspace.resetting.description": "To może potrwać minutę.",
"workspace.reset.failed.title": "Nie udało się zresetować przestrzeni roboczej",
"workspace.reset.success.title": "Przestrzeń robocza zresetowana",
"workspace.reset.success.description": "Przestrzeń robocza odpowiada teraz domyślnej gałęzi.",
"workspace.status.checking": "Sprawdzanie niezscalonych zmian...",
"workspace.status.error": "Nie można zweryfikować statusu git.",
"workspace.status.clean": "Nie wykryto niezscalonych zmian.",
"workspace.status.dirty": "Wykryto niezscalone zmiany w tej przestrzeni roboczej.",
"workspace.delete.title": "Usuń przestrzeń roboczą",
"workspace.delete.confirm": 'Usunąć przestrzeń roboczą "{{name}}"?',
"workspace.delete.button": "Usuń przestrzeń roboczą",
"workspace.reset.title": "Resetuj przestrzeń roboczą",
"workspace.reset.confirm": 'Zresetować przestrzeń roboczą "{{name}}"?',
"workspace.reset.button": "Resetuj przestrzeń roboczą",
"workspace.reset.archived.none": "Żadne aktywne sesje nie zostaną zarchiwizowane.",
"workspace.reset.archived.one": "1 sesja zostanie zarchiwizowana.",
"workspace.reset.archived.many": "{{count}} sesji zostanie zarchiwizowanych.",
"workspace.reset.note": "To zresetuje przestrzeń roboczą, aby odpowiadała domyślnej gałęzi.",
}

684
packages/app/src/i18n/ru.ts Normal file
View File

@@ -0,0 +1,684 @@
export const dict = {
"command.category.suggested": "Предложено",
"command.category.view": "Просмотр",
"command.category.project": "Проект",
"command.category.provider": "Провайдер",
"command.category.server": "Сервер",
"command.category.session": "Сессия",
"command.category.theme": "Тема",
"command.category.language": "Язык",
"command.category.file": "Файл",
"command.category.terminal": "Терминал",
"command.category.model": "Модель",
"command.category.mcp": "MCP",
"command.category.agent": "Агент",
"command.category.permissions": "Разрешения",
"command.category.workspace": "Рабочее пространство",
"command.category.settings": "Настройки",
"theme.scheme.system": "Системная",
"theme.scheme.light": "Светлая",
"theme.scheme.dark": "Тёмная",
"command.sidebar.toggle": "Переключить боковую панель",
"command.project.open": "Открыть проект",
"command.provider.connect": "Подключить провайдера",
"command.server.switch": "Переключить сервер",
"command.settings.open": "Открыть настройки",
"command.session.previous": "Предыдущая сессия",
"command.session.next": "Следующая сессия",
"command.session.archive": "Архивировать сессию",
"command.palette": "Палитра команд",
"command.theme.cycle": "Цикл тем",
"command.theme.set": "Использовать тему: {{theme}}",
"command.theme.scheme.cycle": "Цикл цветовой схемы",
"command.theme.scheme.set": "Использовать цветовую схему: {{scheme}}",
"command.language.cycle": "Цикл языков",
"command.language.set": "Использовать язык: {{language}}",
"command.session.new": "Новая сессия",
"command.file.open": "Открыть файл",
"command.file.open.description": "Поиск файлов и команд",
"command.terminal.toggle": "Переключить терминал",
"command.review.toggle": "Переключить обзор",
"command.terminal.new": "Новый терминал",
"command.terminal.new.description": "Создать новую вкладку терминала",
"command.steps.toggle": "Переключить шаги",
"command.steps.toggle.description": "Показать или скрыть шаги для текущего сообщения",
"command.message.previous": "Предыдущее сообщение",
"command.message.previous.description": "Перейти к предыдущему сообщению пользователя",
"command.message.next": "Следующее сообщение",
"command.message.next.description": "Перейти к следующему сообщению пользователя",
"command.model.choose": "Выбрать модель",
"command.model.choose.description": "Выбрать другую модель",
"command.mcp.toggle": "Переключить MCP",
"command.mcp.toggle.description": "Переключить MCP",
"command.agent.cycle": "Цикл агентов",
"command.agent.cycle.description": "Переключиться к следующему агенту",
"command.agent.cycle.reverse": "Цикл агентов назад",
"command.agent.cycle.reverse.description": "Переключиться к предыдущему агенту",
"command.model.variant.cycle": "Цикл режимов мышления",
"command.model.variant.cycle.description": "Переключиться к следующему уровню усилий",
"command.permissions.autoaccept.enable": "Авто-принятие изменений",
"command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений",
"command.session.undo": "Отменить",
"command.session.undo.description": "Отменить последнее сообщение",
"command.session.redo": "Повторить",
"command.session.redo.description": "Повторить отменённое сообщение",
"command.session.compact": "Сжать сессию",
"command.session.compact.description": "Сократить сессию для уменьшения размера контекста",
"command.session.fork": "Создать ответвление",
"command.session.fork.description": "Создать новую сессию из сообщения",
"command.session.share": "Поделиться сессией",
"command.session.share.description": "Поделиться сессией и скопировать URL в буфер обмена",
"command.session.unshare": "Отменить публикацию",
"command.session.unshare.description": "Прекратить публикацию сессии",
"palette.search.placeholder": "Поиск файлов и команд",
"palette.empty": "Ничего не найдено",
"palette.group.commands": "Команды",
"palette.group.files": "Файлы",
"dialog.provider.search.placeholder": "Поиск провайдеров",
"dialog.provider.empty": "Провайдеры не найдены",
"dialog.provider.group.popular": "Популярные",
"dialog.provider.group.other": "Другие",
"dialog.provider.tag.recommended": "Рекомендуемые",
"dialog.provider.anthropic.note": "Подключитесь с помощью Claude Pro/Max или API ключа",
"dialog.provider.openai.note": "Подключитесь с помощью ChatGPT Pro/Plus или API ключа",
"dialog.provider.copilot.note": "Подключитесь с помощью Copilot или API ключа",
"dialog.model.select.title": "Выбрать модель",
"dialog.model.search.placeholder": "Поиск моделей",
"dialog.model.empty": "Модели не найдены",
"dialog.model.manage": "Управление моделями",
"dialog.model.manage.description": "Настройте какие модели появляются в выборе модели",
"dialog.model.unpaid.freeModels.title": "Бесплатные модели от OpenCode",
"dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров",
"dialog.provider.viewAll": "Показать больше провайдеров",
"provider.connect.title": "Подключить {{provider}}",
"provider.connect.title.anthropicProMax": "Войти с помощью Claude Pro/Max",
"provider.connect.selectMethod": "Выберите способ входа для {{provider}}.",
"provider.connect.method.apiKey": "API ключ",
"provider.connect.status.inProgress": "Авторизация...",
"provider.connect.status.waiting": "Ожидание авторизации...",
"provider.connect.status.failed": "Ошибка авторизации: {{error}}",
"provider.connect.apiKey.description":
"Введите ваш API ключ {{provider}} для подключения аккаунта и использования моделей {{provider}} в OpenCode.",
"provider.connect.apiKey.label": "{{provider}} API ключ",
"provider.connect.apiKey.placeholder": "API ключ",
"provider.connect.apiKey.required": "API ключ обязателен",
"provider.connect.opencodeZen.line1":
"OpenCode Zen даёт вам доступ к отобранным надёжным оптимизированным моделям для агентов программирования.",
"provider.connect.opencodeZen.line2":
"С одним API ключом вы получите доступ к таким моделям как Claude, GPT, Gemini, GLM и другим.",
"provider.connect.opencodeZen.visit.prefix": "Посетите ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": " чтобы получить ваш API ключ.",
"provider.connect.oauth.code.visit.prefix": "Посетите ",
"provider.connect.oauth.code.visit.link": "эту ссылку",
"provider.connect.oauth.code.visit.suffix":
" чтобы получить код авторизации для подключения аккаунта и использования моделей {{provider}} в OpenCode.",
"provider.connect.oauth.code.label": "{{method}} код авторизации",
"provider.connect.oauth.code.placeholder": "Код авторизации",
"provider.connect.oauth.code.required": "Код авторизации обязателен",
"provider.connect.oauth.code.invalid": "Неверный код авторизации",
"provider.connect.oauth.auto.visit.prefix": "Посетите ",
"provider.connect.oauth.auto.visit.link": "эту ссылку",
"provider.connect.oauth.auto.visit.suffix":
" и введите код ниже для подключения аккаунта и использования моделей {{provider}} в OpenCode.",
"provider.connect.oauth.auto.confirmationCode": "Код подтверждения",
"provider.connect.toast.connected.title": "{{provider}} подключён",
"provider.connect.toast.connected.description": "Модели {{provider}} теперь доступны.",
"model.tag.free": "Бесплатно",
"model.tag.latest": "Последняя",
"model.provider.anthropic": "Anthropic",
"model.provider.openai": "OpenAI",
"model.provider.google": "Google",
"model.provider.xai": "xAI",
"model.provider.meta": "Meta",
"model.input.text": "текст",
"model.input.image": "изображение",
"model.input.audio": "аудио",
"model.input.video": "видео",
"model.input.pdf": "pdf",
"model.tooltip.allows": "Разрешено: {{inputs}}",
"model.tooltip.reasoning.allowed": "Разрешает рассуждение",
"model.tooltip.reasoning.none": "Без рассуждения",
"model.tooltip.context": "Лимит контекста {{limit}}",
"common.search.placeholder": "Поиск",
"common.goBack": "Назад",
"common.loading": "Загрузка",
"common.loading.ellipsis": "...",
"common.cancel": "Отмена",
"common.submit": "Отправить",
"common.save": "Сохранить",
"common.saving": "Сохранение...",
"common.default": "По умолчанию",
"common.attachment": "вложение",
"prompt.placeholder.shell": "Введите команду оболочки...",
"prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"',
"prompt.mode.shell": "Оболочка",
"prompt.mode.shell.exit": "esc для выхода",
"prompt.example.1": "Исправить TODO в коде",
"prompt.example.2": "Какой технологический стек этого проекта?",
"prompt.example.3": "Исправить сломанные тесты",
"prompt.example.4": "Объясни как работает аутентификация",
"prompt.example.5": "Найти и исправить уязвимости безопасности",
"prompt.example.6": "Добавить юнит-тесты для сервиса пользователя",
"prompt.example.7": "Рефакторить эту функцию для лучшей читаемости",
"prompt.example.8": "Что означает эта ошибка?",
"prompt.example.9": "Помоги мне отладить эту проблему",
"prompt.example.10": "Сгенерировать документацию API",
"prompt.example.11": "Оптимизировать запросы к базе данных",
"prompt.example.12": "Добавить валидацию ввода",
"prompt.example.13": "Создать новый компонент для...",
"prompt.example.14": "Как развернуть этот проект?",
"prompt.example.15": "Проверь мой код на лучшие практики",
"prompt.example.16": "Добавить обработку ошибок в эту функцию",
"prompt.example.17": "Объясни этот паттерн regex",
"prompt.example.18": "Конвертировать это в TypeScript",
"prompt.example.19": "Добавить логирование по всему проекту",
"prompt.example.20": "Какие зависимости устарели?",
"prompt.example.21": "Помоги написать скрипт миграции",
"prompt.example.22": "Реализовать кэширование для этой конечной точки",
"prompt.example.23": "Добавить пагинацию в этот список",
"prompt.example.24": "Создать CLI команду для...",
"prompt.example.25": "Как работают переменные окружения здесь?",
"prompt.popover.emptyResults": "Нет совпадений",
"prompt.popover.emptyCommands": "Нет совпадающих команд",
"prompt.dropzone.label": "Перетащите изображения или PDF сюда",
"prompt.slash.badge.custom": "своё",
"prompt.context.active": "активно",
"prompt.context.includeActiveFile": "Включить активный файл",
"prompt.context.removeActiveFile": "Удалить активный файл из контекста",
"prompt.context.removeFile": "Удалить файл из контекста",
"prompt.action.attachFile": "Прикрепить файл",
"prompt.attachment.remove": "Удалить вложение",
"prompt.action.send": "Отправить",
"prompt.action.stop": "Остановить",
"prompt.toast.pasteUnsupported.title": "Неподдерживаемая вставка",
"prompt.toast.pasteUnsupported.description": "Сюда можно вставлять только изображения или PDF.",
"prompt.toast.modelAgentRequired.title": "Выберите агента и модель",
"prompt.toast.modelAgentRequired.description": "Выберите агента и модель перед отправкой запроса.",
"prompt.toast.worktreeCreateFailed.title": "Не удалось создать worktree",
"prompt.toast.sessionCreateFailed.title": "Не удалось создать сессию",
"prompt.toast.shellSendFailed.title": "Не удалось отправить команду оболочки",
"prompt.toast.commandSendFailed.title": "Не удалось отправить команду",
"prompt.toast.promptSendFailed.title": "Не удалось отправить запрос",
"dialog.mcp.title": "MCP",
"dialog.mcp.description": "{{enabled}} из {{total}} включено",
"dialog.mcp.empty": "MCP не настроены",
"dialog.lsp.empty": "LSP автоматически обнаружены по типам файлов",
"dialog.plugins.empty": "Плагины настроены в opencode.json",
"mcp.status.connected": "подключено",
"mcp.status.failed": "ошибка",
"mcp.status.needs_auth": "требуется авторизация",
"mcp.status.disabled": "отключено",
"dialog.fork.empty": "Нет сообщений для ответвления",
"dialog.directory.search.placeholder": "Поиск папок",
"dialog.directory.empty": "Папки не найдены",
"dialog.server.title": "Серверы",
"dialog.server.description": "Переключите сервер OpenCode к которому подключается приложение.",
"dialog.server.search.placeholder": "Поиск серверов",
"dialog.server.empty": "Серверов пока нет",
"dialog.server.add.title": "Добавить сервер",
"dialog.server.add.url": "URL сервера",
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "Не удалось подключиться к серверу",
"dialog.server.add.checking": "Проверка...",
"dialog.server.add.button": "Добавить сервер",
"dialog.server.default.title": "Сервер по умолчанию",
"dialog.server.default.description":
"Подключаться к этому серверу при запуске приложения вместо запуска локального сервера. Требуется перезапуск.",
"dialog.server.default.none": "Сервер не выбран",
"dialog.server.default.set": "Установить текущий сервер по умолчанию",
"dialog.server.default.clear": "Очистить",
"dialog.server.action.remove": "Удалить сервер",
"dialog.server.menu.edit": "Редактировать",
"dialog.server.menu.default": "Сделать по умолчанию",
"dialog.server.menu.defaultRemove": "Удалить по умолчанию",
"dialog.server.menu.delete": "Удалить",
"dialog.server.current": "Текущий сервер",
"dialog.server.status.default": "По умолч.",
"dialog.project.edit.title": "Редактировать проект",
"dialog.project.edit.name": "Название",
"dialog.project.edit.icon": "Иконка",
"dialog.project.edit.icon.alt": "Иконка проекта",
"dialog.project.edit.icon.hint": "Нажмите или перетащите изображение",
"dialog.project.edit.icon.recommended": "Рекомендуется: 128x128px",
"dialog.project.edit.color": "Цвет",
"dialog.project.edit.color.select": "Выбрать цвет {{color}}",
"context.breakdown.title": "Разбивка контекста",
"context.breakdown.note":
'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.',
"context.breakdown.system": "Система",
"context.breakdown.user": "Пользователь",
"context.breakdown.assistant": "Ассистент",
"context.breakdown.tool": "Вызовы инструментов",
"context.breakdown.other": "Другое",
"context.systemPrompt.title": "Системный промпт",
"context.rawMessages.title": "Исходные сообщения",
"context.stats.session": "Сессия",
"context.stats.messages": "Сообщения",
"context.stats.provider": "Провайдер",
"context.stats.model": "Модель",
"context.stats.limit": "Лимит контекста",
"context.stats.totalTokens": "Всего токенов",
"context.stats.usage": "Использование",
"context.stats.inputTokens": "Входные токены",
"context.stats.outputTokens": "Выходные токены",
"context.stats.reasoningTokens": "Токены рассуждения",
"context.stats.cacheTokens": "Токены кэша (чтение/запись)",
"context.stats.userMessages": "Сообщения пользователя",
"context.stats.assistantMessages": "Сообщения ассистента",
"context.stats.totalCost": "Общая стоимость",
"context.stats.sessionCreated": "Сессия создана",
"context.stats.lastActivity": "Последняя активность",
"context.usage.tokens": "Токены",
"context.usage.usage": "Использование",
"context.usage.cost": "Стоимость",
"context.usage.clickToView": "Нажмите для просмотра контекста",
"context.usage.view": "Показать использование контекста",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"toast.language.title": "Язык",
"toast.language.description": "Переключено на {{language}}",
"toast.theme.title": "Тема переключена",
"toast.scheme.title": "Цветовая схема",
"toast.permissions.autoaccept.on.title": "Авто-принятие изменений",
"toast.permissions.autoaccept.on.description": "Разрешения на редактирование и запись будут автоматически одобрены",
"toast.permissions.autoaccept.off.title": "Авто-принятие остановлено",
"toast.permissions.autoaccept.off.description": "Редактирование и запись потребуют подтверждения",
"toast.model.none.title": "Модель не выбрана",
"toast.model.none.description": "Подключите провайдера для суммаризации сессии",
"toast.file.loadFailed.title": "Не удалось загрузить файл",
"toast.session.share.copyFailed.title": "Не удалось скопировать URL в буфер обмена",
"toast.session.share.success.title": "Сессия опубликована",
"toast.session.share.success.description": "URL скопирован в буфер обмена!",
"toast.session.share.failed.title": "Не удалось опубликовать сессию",
"toast.session.share.failed.description": "Произошла ошибка при публикации сессии",
"toast.session.unshare.success.title": "Публикация отменена",
"toast.session.unshare.success.description": "Публикация успешно отменена!",
"toast.session.unshare.failed.title": "Не удалось отменить публикацию",
"toast.session.unshare.failed.description": "Произошла ошибка при отмене публикации",
"toast.session.listFailed.title": "Не удалось загрузить сессии для {{project}}",
"toast.update.title": "Доступно обновление",
"toast.update.description": "Новая версия OpenCode ({{version}}) доступна для установки.",
"toast.update.action.installRestart": "Установить и перезапустить",
"toast.update.action.notYet": "Пока нет",
"error.page.title": "Что-то пошло не так",
"error.page.description": "Произошла ошибка при загрузке приложения.",
"error.page.details.label": "Детали ошибки",
"error.page.action.restart": "Перезапустить",
"error.page.action.checking": "Проверка...",
"error.page.action.checkUpdates": "Проверить обновления",
"error.page.action.updateTo": "Обновить до {{version}}",
"error.page.report.prefix": "Пожалуйста, сообщите об этой ошибке команде OpenCode",
"error.page.report.discord": "в Discord",
"error.page.version": "Версия: {{version}}",
"error.dev.rootNotFound":
"Корневой элемент не найден. Вы забыли добавить его в index.html? Или, может быть, атрибут id был написан неправильно?",
"error.globalSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?",
"error.chain.unknown": "Неизвестная ошибка",
"error.chain.causedBy": "Причина:",
"error.chain.apiError": "Ошибка API",
"error.chain.status": "Статус: {{status}}",
"error.chain.retryable": "Повторная попытка: {{retryable}}",
"error.chain.responseBody": "Тело ответа:\n{{body}}",
"error.chain.didYouMean": "Возможно, вы имели в виду: {{suggestions}}",
"error.chain.modelNotFound": "Модель не найдена: {{provider}}/{{model}}",
"error.chain.checkConfig": "Проверьте названия провайдера/модели в конфиге (opencode.json)",
"error.chain.mcpFailed":
'MCP сервер "{{name}}" завершился с ошибкой. Обратите внимание, что OpenCode пока не поддерживает MCP авторизацию.',
"error.chain.providerAuthFailed": "Ошибка аутентификации провайдера ({{provider}}): {{message}}",
"error.chain.providerInitFailed":
'Не удалось инициализировать провайдера "{{provider}}". Проверьте учётные данные и конфигурацию.',
"error.chain.configJsonInvalid": "Конфигурационный файл по адресу {{path}} не является валидным JSON(C)",
"error.chain.configJsonInvalidWithMessage":
"Конфигурационный файл по адресу {{path}} не является валидным JSON(C): {{message}}",
"error.chain.configDirectoryTypo":
'Папка "{{dir}}" в {{path}} невалидна. Переименуйте папку в "{{suggestion}}" или удалите её. Это распространённая опечатка.',
"error.chain.configFrontmatterError": "Не удалось разобрать frontmatter в {{path}}:\n{{message}}",
"error.chain.configInvalid": "Конфигурационный файл по адресу {{path}} невалиден",
"error.chain.configInvalidWithMessage": "Конфигурационный файл по адресу {{path}} невалиден: {{message}}",
"notification.permission.title": "Требуется разрешение",
"notification.permission.description": "{{sessionTitle}} в {{projectName}} требуется разрешение",
"notification.question.title": "Вопрос",
"notification.question.description": "У {{sessionTitle}} в {{projectName}} есть вопрос",
"notification.action.goToSession": "Перейти к сессии",
"notification.session.responseReady.title": "Ответ готов",
"notification.session.error.title": "Ошибка сессии",
"notification.session.error.fallbackDescription": "Произошла ошибка",
"home.recentProjects": "Недавние проекты",
"home.empty.title": "Нет недавних проектов",
"home.empty.description": "Начните с открытия локального проекта",
"session.tab.session": "Сессия",
"session.tab.review": "Обзор",
"session.tab.context": "Контекст",
"session.panel.reviewAndFiles": "Обзор и файлы",
"session.review.filesChanged": "{{count}} файлов изменено",
"session.review.loadingChanges": "Загрузка изменений...",
"session.review.empty": "Изменений в этой сессии пока нет",
"session.messages.renderEarlier": "Показать предыдущие сообщения",
"session.messages.loadingEarlier": "Загрузка предыдущих сообщений...",
"session.messages.loadEarlier": "Загрузить предыдущие сообщения",
"session.messages.loading": "Загрузка сообщений...",
"session.messages.jumpToLatest": "Перейти к последнему",
"session.context.addToContext": "Добавить {{selection}} в контекст",
"session.new.worktree.main": "Основная ветка",
"session.new.worktree.mainWithBranch": "Основная ветка ({{branch}})",
"session.new.worktree.create": "Создать новый worktree",
"session.new.lastModified": "Последнее изменение",
"session.header.search.placeholder": "Поиск {{project}}",
"session.header.searchFiles": "Поиск файлов",
"status.popover.trigger": "Статус",
"status.popover.ariaLabel": "Настройки серверов",
"status.popover.tab.servers": "Серверы",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Плагины",
"status.popover.action.manageServers": "Управлять серверами",
"session.share.popover.title": "Опубликовать в интернете",
"session.share.popover.description.shared":
"Эта сессия общедоступна. Доступ к ней может получить любой, у кого есть ссылка.",
"session.share.popover.description.unshared":
"Опубликуйте сессию в интернете. Доступ к ней сможет получить любой, у кого есть ссылка.",
"session.share.action.share": "Поделиться",
"session.share.action.publish": "Опубликовать",
"session.share.action.publishing": "Публикация...",
"session.share.action.unpublish": "Отменить публикацию",
"session.share.action.unpublishing": "Отмена публикации...",
"session.share.action.view": "Посмотреть",
"session.share.copy.copied": "Скопировано",
"session.share.copy.copyLink": "Копировать ссылку",
"lsp.tooltip.none": "Нет LSP серверов",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Загрузка запроса...",
"terminal.loading": "Загрузка терминала...",
"terminal.title": "Терминал",
"terminal.title.numbered": "Терминал {{number}}",
"terminal.close": "Закрыть терминал",
"terminal.connectionLost.title": "Соединение потеряно",
"terminal.connectionLost.description":
"Соединение с терминалом прервано. Это может произойти при перезапуске сервера.",
"common.closeTab": "Закрыть вкладку",
"common.dismiss": "Закрыть",
"common.requestFailed": "Запрос не выполнен",
"common.moreOptions": "Дополнительные опции",
"common.learnMore": "Подробнее",
"common.rename": "Переименовать",
"common.reset": "Сбросить",
"common.archive": "Архивировать",
"common.delete": "Удалить",
"common.close": "Закрыть",
"common.edit": "Редактировать",
"common.loadMore": "Загрузить ещё",
"common.key.esc": "ESC",
"sidebar.menu.toggle": "Переключить меню",
"sidebar.nav.projectsAndSessions": "Проекты и сессии",
"sidebar.settings": "Настройки",
"sidebar.help": "Помощь",
"sidebar.workspaces.enable": "Включить рабочие пространства",
"sidebar.workspaces.disable": "Отключить рабочие пространства",
"sidebar.gettingStarted.title": "Начало работы",
"sidebar.gettingStarted.line1": "OpenCode включает бесплатные модели, чтобы вы могли начать сразу.",
"sidebar.gettingStarted.line2":
"Подключите любого провайдера для использования моделей, включая Claude, GPT, Gemini и др.",
"sidebar.project.recentSessions": "Недавние сессии",
"sidebar.project.viewAllSessions": "Посмотреть все сессии",
"settings.section.desktop": "Приложение",
"settings.tab.general": "Основные",
"settings.tab.shortcuts": "Горячие клавиши",
"settings.general.section.appearance": "Внешний вид",
"settings.general.section.notifications": "Системные уведомления",
"settings.general.section.sounds": "Звуковые эффекты",
"settings.general.row.language.title": "Язык",
"settings.general.row.language.description": "Изменить язык отображения OpenCode",
"settings.general.row.appearance.title": "Внешний вид",
"settings.general.row.appearance.description": "Настройте как OpenCode выглядит на вашем устройстве",
"settings.general.row.theme.title": "Тема",
"settings.general.row.theme.description": "Настройте оформление OpenCode.",
"settings.general.row.font.title": "Шрифт",
"settings.general.row.font.description": "Настройте моноширинный шрифт для блоков кода",
"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.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": "Alert 01",
"sound.option.alert02": "Alert 02",
"sound.option.alert03": "Alert 03",
"sound.option.alert04": "Alert 04",
"sound.option.alert05": "Alert 05",
"sound.option.alert06": "Alert 06",
"sound.option.alert07": "Alert 07",
"sound.option.alert08": "Alert 08",
"sound.option.alert09": "Alert 09",
"sound.option.alert10": "Alert 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": "Nope 01",
"sound.option.nope02": "Nope 02",
"sound.option.nope03": "Nope 03",
"sound.option.nope04": "Nope 04",
"sound.option.nope05": "Nope 05",
"sound.option.nope06": "Nope 06",
"sound.option.nope07": "Nope 07",
"sound.option.nope08": "Nope 08",
"sound.option.nope09": "Nope 09",
"sound.option.nope10": "Nope 10",
"sound.option.nope11": "Nope 11",
"sound.option.nope12": "Nope 12",
"sound.option.yup01": "Yup 01",
"sound.option.yup02": "Yup 02",
"sound.option.yup03": "Yup 03",
"sound.option.yup04": "Yup 04",
"sound.option.yup05": "Yup 05",
"sound.option.yup06": "Yup 06",
"settings.general.notifications.agent.title": "Агент",
"settings.general.notifications.agent.description":
"Показывать системное уведомление когда агент завершён или требует внимания",
"settings.general.notifications.permissions.title": "Разрешения",
"settings.general.notifications.permissions.description":
"Показывать системное уведомление когда требуется разрешение",
"settings.general.notifications.errors.title": "Ошибки",
"settings.general.notifications.errors.description": "Показывать системное уведомление когда происходит ошибка",
"settings.general.sounds.agent.title": "Агент",
"settings.general.sounds.agent.description": "Воспроизводить звук когда агент завершён или требует внимания",
"settings.general.sounds.permissions.title": "Разрешения",
"settings.general.sounds.permissions.description": "Воспроизводить звук когда требуется разрешение",
"settings.general.sounds.errors.title": "Ошибки",
"settings.general.sounds.errors.description": "Воспроизводить звук когда происходит ошибка",
"settings.shortcuts.title": "Горячие клавиши",
"settings.shortcuts.reset.button": "Сбросить к умолчаниям",
"settings.shortcuts.reset.toast.title": "Горячие клавиши сброшены",
"settings.shortcuts.reset.toast.description": "Горячие клавиши были сброшены к значениям по умолчанию.",
"settings.shortcuts.conflict.title": "Сочетание уже используется",
"settings.shortcuts.conflict.description": "{{keybind}} уже назначено для {{titles}}.",
"settings.shortcuts.unassigned": "Не назначено",
"settings.shortcuts.pressKeys": "Нажмите клавиши",
"settings.shortcuts.search.placeholder": "Поиск горячих клавиш",
"settings.shortcuts.search.empty": "Горячие клавиши не найдены",
"settings.shortcuts.group.general": "Основные",
"settings.shortcuts.group.session": "Сессия",
"settings.shortcuts.group.navigation": "Навигация",
"settings.shortcuts.group.modelAndAgent": "Модель и агент",
"settings.shortcuts.group.terminal": "Терминал",
"settings.shortcuts.group.prompt": "Запрос",
"settings.providers.title": "Провайдеры",
"settings.providers.description": "Настройки провайдеров будут доступны здесь.",
"settings.models.title": "Модели",
"settings.models.description": "Настройки моделей будут доступны здесь.",
"settings.agents.title": "Агенты",
"settings.agents.description": "Настройки агентов будут доступны здесь.",
"settings.commands.title": "Команды",
"settings.commands.description": "Настройки команд будут доступны здесь.",
"settings.mcp.title": "MCP",
"settings.mcp.description": "Настройки MCP будут доступны здесь.",
"settings.permissions.title": "Разрешения",
"settings.permissions.description": "Контролируйте какие инструменты сервер может использовать по умолчанию.",
"settings.permissions.section.tools": "Инструменты",
"settings.permissions.toast.updateFailed.title": "Не удалось обновить разрешения",
"settings.permissions.action.allow": "Разрешить",
"settings.permissions.action.ask": "Спрашивать",
"settings.permissions.action.deny": "Запретить",
"settings.permissions.tool.read.title": "Чтение",
"settings.permissions.tool.read.description": "Чтение файла (по совпадению пути)",
"settings.permissions.tool.edit.title": "Редактирование",
"settings.permissions.tool.edit.description":
"Изменение файлов, включая редактирование, запись, патчи и мульти-редактирование",
"settings.permissions.tool.glob.title": "Glob",
"settings.permissions.tool.glob.description": "Сопоставление файлов по паттернам glob",
"settings.permissions.tool.grep.title": "Grep",
"settings.permissions.tool.grep.description": "Поиск по содержимому файлов с использованием регулярных выражений",
"settings.permissions.tool.list.title": "Список",
"settings.permissions.tool.list.description": "Список файлов в директории",
"settings.permissions.tool.bash.title": "Bash",
"settings.permissions.tool.bash.description": "Выполнение команд оболочки",
"settings.permissions.tool.task.title": "Task",
"settings.permissions.tool.task.description": "Запуск под-агентов",
"settings.permissions.tool.skill.title": "Skill",
"settings.permissions.tool.skill.description": "Загрузить навык по имени",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Выполнение запросов к языковому серверу",
"settings.permissions.tool.todoread.title": "Чтение списка задач",
"settings.permissions.tool.todoread.description": "Чтение списка задач",
"settings.permissions.tool.todowrite.title": "Запись списка задач",
"settings.permissions.tool.todowrite.description": "Обновление списка задач",
"settings.permissions.tool.webfetch.title": "Web Fetch",
"settings.permissions.tool.webfetch.description": "Получить содержимое по URL",
"settings.permissions.tool.websearch.title": "Web Search",
"settings.permissions.tool.websearch.description": "Поиск в интернете",
"settings.permissions.tool.codesearch.title": "Поиск кода",
"settings.permissions.tool.codesearch.description": "Поиск кода в интернете",
"settings.permissions.tool.external_directory.title": "Внешняя директория",
"settings.permissions.tool.external_directory.description": "Доступ к файлам вне директории проекта",
"settings.permissions.tool.doom_loop.title": "Doom Loop",
"settings.permissions.tool.doom_loop.description": "Обнаружение повторных вызовов инструментов с одинаковым вводом",
"session.delete.failed.title": "Не удалось удалить сессию",
"session.delete.title": "Удалить сессию",
"session.delete.confirm": 'Удалить сессию "{{name}}"?',
"session.delete.button": "Удалить сессию",
"workspace.new": "Новое рабочее пространство",
"workspace.type.local": "локальное",
"workspace.type.sandbox": "песочница",
"workspace.create.failed.title": "Не удалось создать рабочее пространство",
"workspace.delete.failed.title": "Не удалось удалить рабочее пространство",
"workspace.resetting.title": "Сброс рабочего пространства",
"workspace.resetting.description": "Это может занять минуту.",
"workspace.reset.failed.title": "Не удалось сбросить рабочее пространство",
"workspace.reset.success.title": "Рабочее пространство сброшено",
"workspace.reset.success.description": "Рабочее пространство теперь соответствует ветке по умолчанию.",
"workspace.status.checking": "Проверка наличия неслитых изменений...",
"workspace.status.error": "Не удалось проверить статус git.",
"workspace.status.clean": "Неслитые изменения не обнаружены.",
"workspace.status.dirty": "Обнаружены неслитые изменения в этом рабочем пространстве.",
"workspace.delete.title": "Удалить рабочее пространство",
"workspace.delete.confirm": 'Удалить рабочее пространство "{{name}}"?',
"workspace.delete.button": "Удалить рабочее пространство",
"workspace.reset.title": "Сбросить рабочее пространство",
"workspace.reset.confirm": 'Сбросить рабочее пространство "{{name}}"?',
"workspace.reset.button": "Сбросить рабочее пространство",
"workspace.reset.archived.none": "Никакие активные сессии не будут архивированы.",
"workspace.reset.archived.one": "1 сессия будет архивирована.",
"workspace.reset.archived.many": "{{count}} сессий будет архивировано.",
"workspace.reset.note": "Рабочее пространство будет сброшено в соответствие с веткой по умолчанию.",
}

View File

@@ -90,6 +90,8 @@ export const dict = {
"dialog.provider.group.other": "其他",
"dialog.provider.tag.recommended": "推荐",
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接",
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接",
"dialog.provider.copilot.note": "使用 Copilot 或 API 密钥连接",
"dialog.model.select.title": "选择模型",
"dialog.model.search.placeholder": "搜索模型",
@@ -100,7 +102,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型",
"dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型",
"dialog.provider.viewAll": "查看全部提供商",
"dialog.provider.viewAll": "查看更多提供商",
"provider.connect.title": "连接 {{provider}}",
"provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登录",
@@ -136,6 +138,7 @@ export const dict = {
"model.tag.latest": "最新",
"common.search.placeholder": "搜索",
"common.goBack": "返回",
"common.loading": "加载中",
"common.cancel": "取消",
"common.submit": "提交",
@@ -181,7 +184,10 @@ export const dict = {
"prompt.slash.badge.custom": "自定义",
"prompt.context.active": "当前",
"prompt.context.includeActiveFile": "包含当前文件",
"prompt.context.removeActiveFile": "从上下文移除活动文件",
"prompt.context.removeFile": "从上下文移除文件",
"prompt.action.attachFile": "附加文件",
"prompt.attachment.remove": "移除附件",
"prompt.action.send": "发送",
"prompt.action.stop": "停止",
@@ -199,6 +205,9 @@ export const dict = {
"dialog.mcp.description": "已启用 {{enabled}} / {{total}}",
"dialog.mcp.empty": "未配置 MCPs",
"dialog.lsp.empty": "已从文件类型自动检测到 LSPs",
"dialog.plugins.empty": "在 opencode.json 中配置的插件",
"mcp.status.connected": "已连接",
"mcp.status.failed": "失败",
"mcp.status.needs_auth": "需要授权",
@@ -218,12 +227,20 @@ export const dict = {
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "无法连接到服务器",
"dialog.server.add.checking": "检查中...",
"dialog.server.add.button": "添加",
"dialog.server.add.button": "添加服务器",
"dialog.server.default.title": "默认服务器",
"dialog.server.default.description": "应用启动时连接此服务器,而不是启动本地服务器。需要重启。",
"dialog.server.default.none": "未选择服务器",
"dialog.server.default.set": "将当前服务器设为默认",
"dialog.server.default.clear": "清除",
"dialog.server.action.remove": "移除服务器",
"dialog.server.menu.edit": "编辑",
"dialog.server.menu.default": "设为默认",
"dialog.server.menu.defaultRemove": "取消默认",
"dialog.server.menu.delete": "删除",
"dialog.server.current": "当前服务器",
"dialog.server.status.default": "默认",
"dialog.project.edit.title": "编辑项目",
"dialog.project.edit.name": "名称",
@@ -232,6 +249,7 @@ export const dict = {
"dialog.project.edit.icon.hint": "点击或拖拽图片",
"dialog.project.edit.icon.recommended": "建议128x128px",
"dialog.project.edit.color": "颜色",
"dialog.project.edit.color.select": "选择{{color}}颜色",
"context.breakdown.title": "上下文拆分",
"context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。",
@@ -265,15 +283,22 @@ export const dict = {
"context.usage.usage": "使用率",
"context.usage.cost": "成本",
"context.usage.clickToView": "点击查看上下文",
"context.usage.view": "查看上下文用量",
"language.en": "英语",
"language.zh": "中文",
"language.ko": "韩语",
"language.de": "德语",
"language.es": "西班牙语",
"language.fr": "法语",
"language.ja": "日语",
"language.da": "丹麦语",
"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)",
"toast.language.title": "语言",
"toast.language.description": "已切换到{{language}}",
@@ -361,6 +386,7 @@ export const dict = {
"session.tab.session": "会话",
"session.tab.review": "审查",
"session.tab.context": "上下文",
"session.panel.reviewAndFiles": "审查和文件",
"session.review.filesChanged": "{{count}} 个文件变更",
"session.review.loadingChanges": "正在加载更改...",
"session.review.empty": "此会话暂无更改",
@@ -377,6 +403,15 @@ export const dict = {
"session.new.lastModified": "最后修改",
"session.header.search.placeholder": "搜索 {{project}}",
"session.header.searchFiles": "搜索文件",
"status.popover.trigger": "状态",
"status.popover.ariaLabel": "服务器配置",
"status.popover.tab.servers": "服务器",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "插件",
"status.popover.action.manageServers": "管理服务器",
"session.share.popover.title": "发布到网页",
"session.share.popover.description.shared": "此会话已在网页上公开。任何拥有链接的人都可以访问。",
@@ -397,6 +432,7 @@ export const dict = {
"terminal.loading": "正在加载终端...",
"terminal.title": "终端",
"terminal.title.numbered": "终端 {{number}}",
"terminal.close": "关闭终端",
"common.closeTab": "关闭标签页",
"common.dismiss": "忽略",
@@ -405,11 +441,13 @@ export const dict = {
"common.learnMore": "了解更多",
"common.rename": "重命名",
"common.reset": "重置",
"common.archive": "归档",
"common.delete": "删除",
"common.close": "关闭",
"common.edit": "编辑",
"common.loadMore": "加载更多",
"sidebar.nav.projectsAndSessions": "项目和会话",
"sidebar.settings": "设置",
"sidebar.help": "帮助",
"sidebar.workspaces.enable": "启用工作区",
@@ -522,6 +560,11 @@ export const dict = {
"settings.permissions.tool.doom_loop.title": "Doom Loop",
"settings.permissions.tool.doom_loop.description": "检测具有相同输入的重复工具调用",
"session.delete.failed.title": "删除会话失败",
"session.delete.title": "删除会话",
"session.delete.confirm": '删除会话 "{{name}}"?',
"session.delete.button": "删除会话",
"workspace.new": "新建工作区",
"workspace.type.local": "本地",
"workspace.type.sandbox": "沙盒",

View File

@@ -0,0 +1,594 @@
import { dict as en } from "./en"
type Keys = keyof typeof en
export const dict = {
"command.category.suggested": "建議",
"command.category.view": "檢視",
"command.category.project": "專案",
"command.category.provider": "提供者",
"command.category.server": "伺服器",
"command.category.session": "工作階段",
"command.category.theme": "主題",
"command.category.language": "語言",
"command.category.file": "檔案",
"command.category.terminal": "終端機",
"command.category.model": "模型",
"command.category.mcp": "MCP",
"command.category.agent": "代理程式",
"command.category.permissions": "權限",
"command.category.workspace": "工作區",
"theme.scheme.system": "系統",
"theme.scheme.light": "淺色",
"theme.scheme.dark": "深色",
"command.sidebar.toggle": "切換側邊欄",
"command.project.open": "開啟專案",
"command.provider.connect": "連接提供者",
"command.server.switch": "切換伺服器",
"command.session.previous": "上一個工作階段",
"command.session.next": "下一個工作階段",
"command.session.archive": "封存工作階段",
"command.palette": "命令面板",
"command.theme.cycle": "循環主題",
"command.theme.set": "使用主題: {{theme}}",
"command.theme.scheme.cycle": "循環配色方案",
"command.theme.scheme.set": "使用配色方案: {{scheme}}",
"command.language.cycle": "循環語言",
"command.language.set": "使用語言: {{language}}",
"command.session.new": "新增工作階段",
"command.file.open": "開啟檔案",
"command.file.open.description": "搜尋檔案和命令",
"command.terminal.toggle": "切換終端機",
"command.review.toggle": "切換審查",
"command.terminal.new": "新增終端機",
"command.terminal.new.description": "建立新的終端機標籤頁",
"command.steps.toggle": "切換步驟",
"command.steps.toggle.description": "顯示或隱藏目前訊息的步驟",
"command.message.previous": "上一則訊息",
"command.message.previous.description": "跳到上一則使用者訊息",
"command.message.next": "下一則訊息",
"command.message.next.description": "跳到下一則使用者訊息",
"command.model.choose": "選擇模型",
"command.model.choose.description": "選擇不同的模型",
"command.mcp.toggle": "切換 MCP",
"command.mcp.toggle.description": "切換 MCP",
"command.agent.cycle": "循環代理程式",
"command.agent.cycle.description": "切換到下一個代理程式",
"command.agent.cycle.reverse": "反向循環代理程式",
"command.agent.cycle.reverse.description": "切換到上一個代理程式",
"command.model.variant.cycle": "循環思考強度",
"command.model.variant.cycle.description": "切換到下一個強度等級",
"command.permissions.autoaccept.enable": "自動接受編輯",
"command.permissions.autoaccept.disable": "停止自動接受編輯",
"command.session.undo": "復原",
"command.session.undo.description": "復原上一則訊息",
"command.session.redo": "重做",
"command.session.redo.description": "重做上一則復原的訊息",
"command.session.compact": "精簡工作階段",
"command.session.compact.description": "總結工作階段以減少上下文大小",
"command.session.fork": "從訊息分支",
"command.session.fork.description": "從先前的訊息建立新工作階段",
"command.session.share": "分享工作階段",
"command.session.share.description": "分享此工作階段並將連結複製到剪貼簿",
"command.session.unshare": "取消分享工作階段",
"command.session.unshare.description": "停止分享此工作階段",
"palette.search.placeholder": "搜尋檔案和命令",
"palette.empty": "找不到結果",
"palette.group.commands": "命令",
"palette.group.files": "檔案",
"dialog.provider.search.placeholder": "搜尋提供者",
"dialog.provider.empty": "找不到提供者",
"dialog.provider.group.popular": "熱門",
"dialog.provider.group.other": "其他",
"dialog.provider.tag.recommended": "推薦",
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 金鑰連線",
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 金鑰連線",
"dialog.provider.copilot.note": "使用 Copilot 或 API 金鑰連線",
"dialog.model.select.title": "選擇模型",
"dialog.model.search.placeholder": "搜尋模型",
"dialog.model.empty": "找不到模型",
"dialog.model.manage": "管理模型",
"dialog.model.manage.description": "自訂模型選擇器中顯示的模型。",
"dialog.model.unpaid.freeModels.title": "OpenCode 提供的免費模型",
"dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型",
"dialog.provider.viewAll": "查看更多提供者",
"provider.connect.title": "連線 {{provider}}",
"provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登入",
"provider.connect.selectMethod": "選擇 {{provider}} 的登入方式。",
"provider.connect.method.apiKey": "API 金鑰",
"provider.connect.status.inProgress": "正在授權...",
"provider.connect.status.waiting": "等待授權...",
"provider.connect.status.failed": "授權失敗: {{error}}",
"provider.connect.apiKey.description":
"輸入你的 {{provider}} API 金鑰以連線帳戶,並在 OpenCode 中使用 {{provider}} 模型。",
"provider.connect.apiKey.label": "{{provider}} API 金鑰",
"provider.connect.apiKey.placeholder": "API 金鑰",
"provider.connect.apiKey.required": "API 金鑰為必填",
"provider.connect.opencodeZen.line1": "OpenCode Zen 為你提供一組精選的可靠最佳化模型,用於程式碼代理程式。",
"provider.connect.opencodeZen.line2": "只需一個 API 金鑰,你就能使用 Claude、GPT、Gemini、GLM 等模型。",
"provider.connect.opencodeZen.visit.prefix": "造訪 ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": " 取得你的 API 金鑰。",
"provider.connect.oauth.code.visit.prefix": "造訪 ",
"provider.connect.oauth.code.visit.link": "此連結",
"provider.connect.oauth.code.visit.suffix": " 取得授權碼,以連線你的帳戶並在 OpenCode 中使用 {{provider}} 模型。",
"provider.connect.oauth.code.label": "{{method}} 授權碼",
"provider.connect.oauth.code.placeholder": "授權碼",
"provider.connect.oauth.code.required": "授權碼為必填",
"provider.connect.oauth.code.invalid": "授權碼無效",
"provider.connect.oauth.auto.visit.prefix": "造訪 ",
"provider.connect.oauth.auto.visit.link": "此連結",
"provider.connect.oauth.auto.visit.suffix":
" 並輸入以下程式碼,以連線你的帳戶並在 OpenCode 中使用 {{provider}} 模型。",
"provider.connect.oauth.auto.confirmationCode": "確認碼",
"provider.connect.toast.connected.title": "{{provider}} 已連線",
"provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。",
"model.tag.free": "免費",
"model.tag.latest": "最新",
"common.search.placeholder": "搜尋",
"common.goBack": "返回",
"common.loading": "載入中",
"common.cancel": "取消",
"common.submit": "提交",
"common.save": "儲存",
"common.saving": "儲存中...",
"common.default": "預設",
"common.attachment": "附件",
"prompt.placeholder.shell": "輸入 shell 命令...",
"prompt.placeholder.normal": '隨便問點什麼... "{{example}}"',
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "按 esc 退出",
"prompt.example.1": "修復程式碼庫中的一個 TODO",
"prompt.example.2": "這個專案的技術堆疊是什麼?",
"prompt.example.3": "修復失敗的測試",
"prompt.example.4": "解釋驗證是如何運作的",
"prompt.example.5": "尋找並修復安全漏洞",
"prompt.example.6": "為使用者服務新增單元測試",
"prompt.example.7": "重構這個函式,讓它更易讀",
"prompt.example.8": "這個錯誤是什麼意思?",
"prompt.example.9": "幫我偵錯這個問題",
"prompt.example.10": "產生 API 文件",
"prompt.example.11": "最佳化資料庫查詢",
"prompt.example.12": "新增輸入驗證",
"prompt.example.13": "建立一個新的元件用於...",
"prompt.example.14": "我該如何部署這個專案?",
"prompt.example.15": "審查我的程式碼並給出最佳實務建議",
"prompt.example.16": "為這個函式新增錯誤處理",
"prompt.example.17": "解釋這個正規表示式",
"prompt.example.18": "把它轉換成 TypeScript",
"prompt.example.19": "在整個程式碼庫中新增日誌",
"prompt.example.20": "哪些相依性已經過期?",
"prompt.example.21": "幫我寫一個遷移腳本",
"prompt.example.22": "為這個端點實作快取",
"prompt.example.23": "給這個清單新增分頁",
"prompt.example.24": "建立一個 CLI 命令用於...",
"prompt.example.25": "這裡的環境變數是怎麼運作的?",
"prompt.popover.emptyResults": "沒有符合的結果",
"prompt.popover.emptyCommands": "沒有符合的命令",
"prompt.dropzone.label": "將圖片或 PDF 拖到這裡",
"prompt.slash.badge.custom": "自訂",
"prompt.context.active": "作用中",
"prompt.context.includeActiveFile": "包含作用中檔案",
"prompt.context.removeActiveFile": "從上下文移除目前檔案",
"prompt.context.removeFile": "從上下文移除檔案",
"prompt.action.attachFile": "附加檔案",
"prompt.attachment.remove": "移除附件",
"prompt.action.send": "傳送",
"prompt.action.stop": "停止",
"prompt.toast.pasteUnsupported.title": "不支援的貼上",
"prompt.toast.pasteUnsupported.description": "這裡只能貼上圖片或 PDF 檔案。",
"prompt.toast.modelAgentRequired.title": "請選擇代理程式和模型",
"prompt.toast.modelAgentRequired.description": "傳送提示前請先選擇代理程式和模型。",
"prompt.toast.worktreeCreateFailed.title": "建立工作樹失敗",
"prompt.toast.sessionCreateFailed.title": "建立工作階段失敗",
"prompt.toast.shellSendFailed.title": "傳送 shell 命令失敗",
"prompt.toast.commandSendFailed.title": "傳送命令失敗",
"prompt.toast.promptSendFailed.title": "傳送提示失敗",
"dialog.mcp.title": "MCP",
"dialog.mcp.description": "已啟用 {{enabled}} / {{total}}",
"dialog.mcp.empty": "未設定 MCP",
"dialog.lsp.empty": "已從檔案類型自動偵測到 LSPs",
"dialog.plugins.empty": "在 opencode.json 中設定的外掛程式",
"mcp.status.connected": "已連線",
"mcp.status.failed": "失敗",
"mcp.status.needs_auth": "需要授權",
"mcp.status.disabled": "已停用",
"dialog.fork.empty": "沒有可用於分支的訊息",
"dialog.directory.search.placeholder": "搜尋資料夾",
"dialog.directory.empty": "找不到資料夾",
"dialog.server.title": "伺服器",
"dialog.server.description": "切換此應用程式連線的 OpenCode 伺服器。",
"dialog.server.search.placeholder": "搜尋伺服器",
"dialog.server.empty": "暫無伺服器",
"dialog.server.add.title": "新增伺服器",
"dialog.server.add.url": "伺服器 URL",
"dialog.server.add.placeholder": "http://localhost:4096",
"dialog.server.add.error": "無法連線到伺服器",
"dialog.server.add.checking": "檢查中...",
"dialog.server.add.button": "新增伺服器",
"dialog.server.default.title": "預設伺服器",
"dialog.server.default.description": "應用程式啟動時連線此伺服器,而不是啟動本地伺服器。需要重新啟動。",
"dialog.server.default.none": "未選擇伺服器",
"dialog.server.default.set": "將目前伺服器設為預設",
"dialog.server.default.clear": "清除",
"dialog.server.action.remove": "移除伺服器",
"dialog.server.menu.edit": "編輯",
"dialog.server.menu.default": "設為預設",
"dialog.server.menu.defaultRemove": "取消預設",
"dialog.server.menu.delete": "刪除",
"dialog.server.current": "目前伺服器",
"dialog.server.status.default": "預設",
"dialog.project.edit.title": "編輯專案",
"dialog.project.edit.name": "名稱",
"dialog.project.edit.icon": "圖示",
"dialog.project.edit.icon.alt": "專案圖示",
"dialog.project.edit.icon.hint": "點擊或拖曳圖片",
"dialog.project.edit.icon.recommended": "建議128x128px",
"dialog.project.edit.color": "顏色",
"dialog.project.edit.color.select": "選擇{{color}}顏色",
"context.breakdown.title": "上下文拆分",
"context.breakdown.note": "輸入 token 的大致拆分。「其他」包含工具定義和額外開銷。",
"context.breakdown.system": "系統",
"context.breakdown.user": "使用者",
"context.breakdown.assistant": "助手",
"context.breakdown.tool": "工具呼叫",
"context.breakdown.other": "其他",
"context.systemPrompt.title": "系統提示詞",
"context.rawMessages.title": "原始訊息",
"context.stats.session": "工作階段",
"context.stats.messages": "訊息數",
"context.stats.provider": "提供者",
"context.stats.model": "模型",
"context.stats.limit": "上下文限制",
"context.stats.totalTokens": "總 token",
"context.stats.usage": "使用量",
"context.stats.inputTokens": "輸入 token",
"context.stats.outputTokens": "輸出 token",
"context.stats.reasoningTokens": "推理 token",
"context.stats.cacheTokens": "快取 token讀/寫)",
"context.stats.userMessages": "使用者訊息",
"context.stats.assistantMessages": "助手訊息",
"context.stats.totalCost": "總成本",
"context.stats.sessionCreated": "建立時間",
"context.stats.lastActivity": "最後活動",
"context.usage.tokens": "Token",
"context.usage.usage": "使用量",
"context.usage.cost": "成本",
"context.usage.clickToView": "點擊查看上下文",
"context.usage.view": "檢視上下文用量",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"toast.language.title": "語言",
"toast.language.description": "已切換到 {{language}}",
"toast.theme.title": "主題已切換",
"toast.scheme.title": "配色方案",
"toast.permissions.autoaccept.on.title": "自動接受編輯",
"toast.permissions.autoaccept.on.description": "編輯和寫入權限將自動獲准",
"toast.permissions.autoaccept.off.title": "已停止自動接受編輯",
"toast.permissions.autoaccept.off.description": "編輯和寫入權限將需要手動批准",
"toast.model.none.title": "未選擇模型",
"toast.model.none.description": "請先連線提供者以總結此工作階段",
"toast.file.loadFailed.title": "載入檔案失敗",
"toast.session.share.copyFailed.title": "無法複製連結到剪貼簿",
"toast.session.share.success.title": "工作階段已分享",
"toast.session.share.success.description": "分享連結已複製到剪貼簿",
"toast.session.share.failed.title": "分享工作階段失敗",
"toast.session.share.failed.description": "分享工作階段時發生錯誤",
"toast.session.unshare.success.title": "已取消分享工作階段",
"toast.session.unshare.success.description": "工作階段已成功取消分享",
"toast.session.unshare.failed.title": "取消分享失敗",
"toast.session.unshare.failed.description": "取消分享工作階段時發生錯誤",
"toast.session.listFailed.title": "無法載入 {{project}} 的工作階段",
"toast.update.title": "有可用更新",
"toast.update.description": "OpenCode 有新版本 ({{version}}) 可安裝。",
"toast.update.action.installRestart": "安裝並重新啟動",
"toast.update.action.notYet": "稍後",
"error.page.title": "出了點問題",
"error.page.description": "載入應用程式時發生錯誤。",
"error.page.details.label": "錯誤詳情",
"error.page.action.restart": "重新啟動",
"error.page.action.checking": "檢查中...",
"error.page.action.checkUpdates": "檢查更新",
"error.page.action.updateTo": "更新到 {{version}}",
"error.page.report.prefix": "請將此錯誤回報給 OpenCode 團隊",
"error.page.report.discord": "在 Discord 上",
"error.page.version": "版本: {{version}}",
"error.dev.rootNotFound": "找不到根元素。你是不是忘了把它新增到 index.html? 或者 id 屬性拼錯了?",
"error.globalSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?",
"error.chain.unknown": "未知錯誤",
"error.chain.causedBy": "原因:",
"error.chain.apiError": "API 錯誤",
"error.chain.status": "狀態: {{status}}",
"error.chain.retryable": "可重試: {{retryable}}",
"error.chain.responseBody": "回應內容:\n{{body}}",
"error.chain.didYouMean": "你是不是想輸入: {{suggestions}}",
"error.chain.modelNotFound": "找不到模型: {{provider}}/{{model}}",
"error.chain.checkConfig": "請檢查你的設定 (opencode.json) 中的 provider/model 名稱",
"error.chain.mcpFailed": 'MCP 伺服器 "{{name}}" 啟動失敗。注意: OpenCode 暫不支援 MCP 認證。',
"error.chain.providerAuthFailed": "提供者認證失敗 ({{provider}}): {{message}}",
"error.chain.providerInitFailed": '無法初始化提供者 "{{provider}}"。請檢查憑證和設定。',
"error.chain.configJsonInvalid": "設定檔 {{path}} 不是有效的 JSON(C)",
"error.chain.configJsonInvalidWithMessage": "設定檔 {{path}} 不是有效的 JSON(C): {{message}}",
"error.chain.configDirectoryTypo":
'{{path}} 中的目錄 "{{dir}}" 無效。請將目錄重新命名為 "{{suggestion}}" 或移除它。這是一個常見拼寫錯誤。',
"error.chain.configFrontmatterError": "無法解析 {{path}} 中的 frontmatter:\n{{message}}",
"error.chain.configInvalid": "設定檔 {{path}} 無效",
"error.chain.configInvalidWithMessage": "設定檔 {{path}} 無效: {{message}}",
"notification.permission.title": "需要權限",
"notification.permission.description": "{{sessionTitle}}{{projectName}})需要權限",
"notification.question.title": "問題",
"notification.question.description": "{{sessionTitle}}{{projectName}})有一個問題",
"notification.action.goToSession": "前往工作階段",
"notification.session.responseReady.title": "回覆已就緒",
"notification.session.error.title": "工作階段錯誤",
"notification.session.error.fallbackDescription": "發生錯誤",
"home.recentProjects": "最近專案",
"home.empty.title": "沒有最近專案",
"home.empty.description": "透過開啟本地專案開始使用",
"session.tab.session": "工作階段",
"session.tab.review": "審查",
"session.tab.context": "上下文",
"session.panel.reviewAndFiles": "審查與檔案",
"session.review.filesChanged": "{{count}} 個檔案變更",
"session.review.loadingChanges": "正在載入變更...",
"session.review.empty": "此工作階段暫無變更",
"session.messages.renderEarlier": "顯示更早的訊息",
"session.messages.loadingEarlier": "正在載入更早的訊息...",
"session.messages.loadEarlier": "載入更早的訊息",
"session.messages.loading": "正在載入訊息...",
"session.context.addToContext": "將 {{selection}} 新增到上下文",
"session.new.worktree.main": "主分支",
"session.new.worktree.mainWithBranch": "主分支 ({{branch}})",
"session.new.worktree.create": "建立新的 worktree",
"session.new.lastModified": "最後修改",
"session.header.search.placeholder": "搜尋 {{project}}",
"session.header.searchFiles": "搜尋檔案",
"status.popover.trigger": "狀態",
"status.popover.ariaLabel": "伺服器設定",
"status.popover.tab.servers": "伺服器",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "外掛程式",
"status.popover.action.manageServers": "管理伺服器",
"session.share.popover.title": "發佈到網頁",
"session.share.popover.description.shared": "此工作階段已在網頁上公開。任何擁有連結的人都可以存取。",
"session.share.popover.description.unshared": "在網頁上公開分享此工作階段。任何擁有連結的人都可以存取。",
"session.share.action.share": "分享",
"session.share.action.publish": "發佈",
"session.share.action.publishing": "正在發佈...",
"session.share.action.unpublish": "取消發佈",
"session.share.action.unpublishing": "正在取消發佈...",
"session.share.action.view": "檢視",
"session.share.copy.copied": "已複製",
"session.share.copy.copyLink": "複製連結",
"lsp.tooltip.none": "沒有 LSP 伺服器",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "正在載入提示...",
"terminal.loading": "正在載入終端機...",
"terminal.title": "終端機",
"terminal.title.numbered": "終端機 {{number}}",
"terminal.close": "關閉終端機",
"common.closeTab": "關閉標籤頁",
"common.dismiss": "忽略",
"common.requestFailed": "要求失敗",
"common.moreOptions": "更多選項",
"common.learnMore": "深入了解",
"common.rename": "重新命名",
"common.reset": "重設",
"common.archive": "封存",
"common.delete": "刪除",
"common.close": "關閉",
"common.edit": "編輯",
"common.loadMore": "載入更多",
"sidebar.nav.projectsAndSessions": "專案與工作階段",
"sidebar.settings": "設定",
"sidebar.help": "說明",
"sidebar.workspaces.enable": "啟用工作區",
"sidebar.workspaces.disable": "停用工作區",
"sidebar.gettingStarted.title": "開始使用",
"sidebar.gettingStarted.line1": "OpenCode 提供免費模型,你可以立即開始使用。",
"sidebar.gettingStarted.line2": "連線任意提供者即可使用更多模型,如 Claude、GPT、Gemini 等。",
"sidebar.project.recentSessions": "最近工作階段",
"sidebar.project.viewAllSessions": "查看全部工作階段",
"settings.section.desktop": "桌面",
"settings.tab.general": "一般",
"settings.tab.shortcuts": "快速鍵",
"settings.general.section.appearance": "外觀",
"settings.general.section.notifications": "系統通知",
"settings.general.section.sounds": "音效",
"settings.general.row.language.title": "語言",
"settings.general.row.language.description": "變更 OpenCode 的顯示語言",
"settings.general.row.appearance.title": "外觀",
"settings.general.row.appearance.description": "自訂 OpenCode 在你的裝置上的外觀",
"settings.general.row.theme.title": "主題",
"settings.general.row.theme.description": "自訂 OpenCode 的主題。",
"settings.general.row.font.title": "字型",
"settings.general.row.font.description": "自訂程式碼區塊使用的等寬字型",
"settings.general.notifications.agent.title": "代理程式",
"settings.general.notifications.agent.description": "當代理程式完成或需要注意時顯示系統通知",
"settings.general.notifications.permissions.title": "權限",
"settings.general.notifications.permissions.description": "當需要權限時顯示系統通知",
"settings.general.notifications.errors.title": "錯誤",
"settings.general.notifications.errors.description": "發生錯誤時顯示系統通知",
"settings.general.sounds.agent.title": "代理程式",
"settings.general.sounds.agent.description": "當代理程式完成或需要注意時播放聲音",
"settings.general.sounds.permissions.title": "權限",
"settings.general.sounds.permissions.description": "當需要權限時播放聲音",
"settings.general.sounds.errors.title": "錯誤",
"settings.general.sounds.errors.description": "發生錯誤時播放聲音",
"settings.shortcuts.title": "鍵盤快速鍵",
"settings.shortcuts.reset.button": "重設為預設值",
"settings.shortcuts.reset.toast.title": "快速鍵已重設",
"settings.shortcuts.reset.toast.description": "鍵盤快速鍵已重設為預設設定。",
"settings.shortcuts.conflict.title": "快速鍵已被占用",
"settings.shortcuts.conflict.description": "{{keybind}} 已分配給 {{titles}}。",
"settings.shortcuts.unassigned": "未設定",
"settings.shortcuts.pressKeys": "按下按鍵",
"settings.shortcuts.search.placeholder": "搜尋快速鍵",
"settings.shortcuts.search.empty": "找不到快速鍵",
"settings.shortcuts.group.general": "一般",
"settings.shortcuts.group.session": "工作階段",
"settings.shortcuts.group.navigation": "導覽",
"settings.shortcuts.group.modelAndAgent": "模型與代理程式",
"settings.shortcuts.group.terminal": "終端機",
"settings.shortcuts.group.prompt": "提示",
"settings.providers.title": "提供者",
"settings.providers.description": "提供者設定將在此處可設定。",
"settings.models.title": "模型",
"settings.models.description": "模型設定將在此處可設定。",
"settings.agents.title": "代理程式",
"settings.agents.description": "代理程式設定將在此處可設定。",
"settings.commands.title": "命令",
"settings.commands.description": "命令設定將在此處可設定。",
"settings.mcp.title": "MCP",
"settings.mcp.description": "MCP 設定將在此處可設定。",
"settings.permissions.title": "權限",
"settings.permissions.description": "控制伺服器預設可以使用哪些工具。",
"settings.permissions.section.tools": "工具",
"settings.permissions.toast.updateFailed.title": "更新權限失敗",
"settings.permissions.action.allow": "允許",
"settings.permissions.action.ask": "詢問",
"settings.permissions.action.deny": "拒絕",
"settings.permissions.tool.read.title": "讀取",
"settings.permissions.tool.read.description": "讀取檔案(符合檔案路徑)",
"settings.permissions.tool.edit.title": "編輯",
"settings.permissions.tool.edit.description": "修改檔案,包括編輯、寫入、修補和多重編輯",
"settings.permissions.tool.glob.title": "Glob",
"settings.permissions.tool.glob.description": "使用 glob 模式符合檔案",
"settings.permissions.tool.grep.title": "Grep",
"settings.permissions.tool.grep.description": "使用正規表示式搜尋檔案內容",
"settings.permissions.tool.list.title": "清單",
"settings.permissions.tool.list.description": "列出目錄中的檔案",
"settings.permissions.tool.bash.title": "Bash",
"settings.permissions.tool.bash.description": "執行 shell 命令",
"settings.permissions.tool.task.title": "Task",
"settings.permissions.tool.task.description": "啟動子代理程式",
"settings.permissions.tool.skill.title": "Skill",
"settings.permissions.tool.skill.description": "按名稱載入技能",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "執行語言伺服器查詢",
"settings.permissions.tool.todoread.title": "讀取待辦",
"settings.permissions.tool.todoread.description": "讀取待辦清單",
"settings.permissions.tool.todowrite.title": "更新待辦",
"settings.permissions.tool.todowrite.description": "更新待辦清單",
"settings.permissions.tool.webfetch.title": "Web Fetch",
"settings.permissions.tool.webfetch.description": "從 URL 取得內容",
"settings.permissions.tool.websearch.title": "Web Search",
"settings.permissions.tool.websearch.description": "搜尋網頁",
"settings.permissions.tool.codesearch.title": "Code Search",
"settings.permissions.tool.codesearch.description": "在網路上搜尋程式碼",
"settings.permissions.tool.external_directory.title": "外部目錄",
"settings.permissions.tool.external_directory.description": "存取專案目錄之外的檔案",
"settings.permissions.tool.doom_loop.title": "Doom Loop",
"settings.permissions.tool.doom_loop.description": "偵測具有相同輸入的重複工具呼叫",
"session.delete.failed.title": "刪除工作階段失敗",
"session.delete.title": "刪除工作階段",
"session.delete.confirm": '刪除工作階段 "{{name}}"?',
"session.delete.button": "刪除工作階段",
"workspace.new": "新增工作區",
"workspace.type.local": "本地",
"workspace.type.sandbox": "沙盒",
"workspace.create.failed.title": "建立工作區失敗",
"workspace.delete.failed.title": "刪除工作區失敗",
"workspace.resetting.title": "正在重設工作區",
"workspace.resetting.description": "這可能需要一點時間。",
"workspace.reset.failed.title": "重設工作區失敗",
"workspace.reset.success.title": "工作區已重設",
"workspace.reset.success.description": "工作區已與預設分支保持一致。",
"workspace.status.checking": "正在檢查未合併的變更...",
"workspace.status.error": "無法驗證 git 狀態。",
"workspace.status.clean": "未偵測到未合併的變更。",
"workspace.status.dirty": "偵測到未合併的變更。",
"workspace.delete.title": "刪除工作區",
"workspace.delete.confirm": '刪除工作區 "{{name}}"?',
"workspace.delete.button": "刪除工作區",
"workspace.reset.title": "重設工作區",
"workspace.reset.confirm": '重設工作區 "{{name}}"?',
"workspace.reset.button": "重設工作區",
"workspace.reset.archived.none": "不會封存任何作用中工作階段。",
"workspace.reset.archived.one": "將封存 1 個工作階段。",
"workspace.reset.archived.many": "將封存 {{count}} 個工作階段。",
"workspace.reset.note": "這將把工作區重設為與預設分支一致。",
} satisfies Partial<Record<Keys, string>>

View File

@@ -29,3 +29,56 @@
*[data-tauri-drag-region] {
app-region: drag;
}
.session-scroller::-webkit-scrollbar {
width: 10px !important;
height: 10px !important;
}
.session-scroller::-webkit-scrollbar-track {
background: transparent !important;
border-radius: 5px !important;
}
.session-scroller::-webkit-scrollbar-thumb {
background: var(--border-weak-base) !important;
border-radius: 5px !important;
border: 3px solid transparent !important;
background-clip: padding-box !important;
}
.session-scroller::-webkit-scrollbar-thumb:hover {
background: var(--border-weak-base) !important;
}
.session-scroller {
scrollbar-width: thin !important;
scrollbar-color: var(--border-weak-base) transparent !important;
}
/* Wider dialog variant for release notes modal */
[data-component="dialog"]:has(.dialog-release-notes) {
padding: 20px;
box-sizing: border-box;
[data-slot="dialog-container"] {
width: min(100%, 720px);
height: min(100%, 400px);
margin-top: -80px;
[data-slot="dialog-content"] {
min-height: auto;
overflow: hidden;
height: 100%;
border: none;
box-shadow: var(--shadow-lg-border-base);
}
[data-slot="dialog-body"] {
overflow: hidden;
height: 100%;
display: flex;
flex-direction: row;
}
}
}

View File

@@ -0,0 +1,53 @@
import { CURRENT_RELEASE } from "@/components/dialog-release-notes"
const STORAGE_KEY = "opencode:last-seen-version"
// ============================================================================
// DEV MODE: Set this to true to always show the release notes modal on startup
// Set to false for production behavior (only shows after updates)
// ============================================================================
const DEV_ALWAYS_SHOW_RELEASE_NOTES = true
/**
* Check if release notes should be shown
* Returns true if:
* - DEV_ALWAYS_SHOW_RELEASE_NOTES is true (for development)
* - OR the current version is newer than the last seen version
*/
export function shouldShowReleaseNotes(): boolean {
if (DEV_ALWAYS_SHOW_RELEASE_NOTES) {
console.log("[ReleaseNotes] DEV mode: always showing release notes")
return true
}
const lastSeen = localStorage.getItem(STORAGE_KEY)
if (!lastSeen) {
// First time user - show release notes
return true
}
// Compare versions - show if current is newer
return CURRENT_RELEASE.version !== lastSeen
}
/**
* Mark the current release notes as seen
* Call this when the user closes the release notes modal
*/
export function markReleaseNotesSeen(): void {
localStorage.setItem(STORAGE_KEY, CURRENT_RELEASE.version)
}
/**
* Get the current version
*/
export function getCurrentVersion(): string {
return CURRENT_RELEASE.version
}
/**
* Reset the seen status (useful for testing)
*/
export function resetReleaseNotesSeen(): void {
localStorage.removeItem(STORAGE_KEY)
}

View File

@@ -16,7 +16,7 @@ export default function Layout(props: ParentProps) {
return base64Decode(params.dir!)
})
return (
<Show when={params.dir} keyed>
<Show when={params.dir}>
<SDKProvider directory={directory()}>
<SyncProvider>
{iife(() => {

View File

@@ -1,4 +1,4 @@
import { createMemo, For, Match, Show, Switch } from "solid-js"
import { createMemo, For, Match, Switch } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { Logo } from "@opencode-ai/ui/logo"
import { useLayout } from "@/context/layout"

View File

@@ -56,6 +56,7 @@ import { usePermission } from "@/context/permission"
import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { playSound, soundSrc } from "@/utils/sound"
import { Worktree as WorktreeState } from "@/utils/worktree"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
@@ -67,6 +68,7 @@ import { ConstrainDragXAxis } from "@/utils/solid-dnd"
import { navStart } from "@/utils/perf"
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
import { DialogEditProject } from "@/components/dialog-edit-project"
import { ReleaseNotesHandler } from "@/components/release-notes-handler"
import { Titlebar } from "@/components/titlebar"
import { useServer } from "@/context/server"
import { useLanguage, type Locale } from "@/context/language"
@@ -88,11 +90,6 @@ export default function Layout(props: ParentProps) {
const pageReady = createMemo(() => ready())
let scrollContainerRef: HTMLDivElement | undefined
const xlQuery = window.matchMedia("(min-width: 1280px)")
const [isLargeViewport, setIsLargeViewport] = createSignal(xlQuery.matches)
const handleViewportChange = (e: MediaQueryListEvent) => setIsLargeViewport(e.matches)
xlQuery.addEventListener("change", handleViewportChange)
onCleanup(() => xlQuery.removeEventListener("change", handleViewportChange))
const params = useParams()
const [autoselect, setAutoselect] = createSignal(!params.dir)
@@ -332,6 +329,18 @@ export default function Layout(props: ParentProps) {
const cooldownMs = 5000
const unsub = globalSDK.event.listen((e) => {
if (e.details?.type === "worktree.ready") {
setBusy(e.name, false)
WorktreeState.ready(e.name)
return
}
if (e.details?.type === "worktree.failed") {
setBusy(e.name, false)
WorktreeState.failed(e.name, e.details.properties?.message ?? language.t("common.requestFailed"))
return
}
if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return
const title =
e.details.type === "permission.asked"
@@ -537,11 +546,10 @@ export default function Layout(props: ParentProps) {
const workspaceLabel = (directory: string, branch?: string, projectId?: string) =>
workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
const isWorkspaceEditing = () => editor.active.startsWith("workspace:")
const workspaceSetting = createMemo(() => {
const project = currentProject()
if (!project) return false
if (project.vcs !== "git") return false
return layout.sidebar.workspaces(project.worktree)()
})
@@ -551,6 +559,7 @@ export default function Layout(props: ParentProps) {
const project = currentProject()
if (!project) return
const local = project.worktree
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
const existing = store.workspaceOrder[project.worktree]
if (!existing) {
@@ -558,9 +567,9 @@ export default function Layout(props: ParentProps) {
return
}
const keep = existing.filter((d) => dirs.includes(d))
const missing = dirs.filter((d) => !existing.includes(d))
const merged = [...keep, ...missing]
const keep = existing.filter((d) => d !== local && dirs.includes(d))
const missing = dirs.filter((d) => d !== local && !existing.includes(d))
const merged = [local, ...missing, ...keep]
if (merged.length !== existing.length) {
setStore("workspaceOrder", project.worktree, merged)
@@ -580,7 +589,7 @@ export default function Layout(props: ParentProps) {
if (!expanded) continue
const project = projects.find((item) => item.worktree === directory || item.sandboxes?.includes(directory))
if (!project) continue
if (layout.sidebar.workspaces(project.worktree)()) continue
if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue
setStore("workspaceExpanded", directory, false)
}
})
@@ -710,7 +719,8 @@ export default function Layout(props: ParentProps) {
if (!directory) return
const [store] = globalSync.child(directory)
if (store.message[session.id] !== undefined) return
const cached = untrack(() => store.message[session.id] !== undefined)
if (cached) return
const q = queueFor(directory)
if (q.inflight.has(session.id)) return
@@ -819,6 +829,69 @@ export default function Layout(props: ParentProps) {
}
}
async function deleteSession(session: Session) {
const [store, setStore] = globalSync.child(session.directory)
const sessions = (store.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
const index = sessions.findIndex((s) => s.id === session.id)
const nextSession = sessions[index + 1] ?? sessions[index - 1]
const result = await globalSDK.client.session
.delete({ directory: session.directory, sessionID: session.id })
.then((x) => x.data)
.catch((err) => {
showToast({
title: language.t("session.delete.failed.title"),
description: errorMessage(err),
})
return false
})
if (!result) return
setStore(
produce((draft) => {
const removed = new Set<string>([session.id])
const byParent = new Map<string, string[]>()
for (const item of draft.session) {
const parentID = item.parentID
if (!parentID) continue
const existing = byParent.get(parentID)
if (existing) {
existing.push(item.id)
continue
}
byParent.set(parentID, [item.id])
}
const stack = [session.id]
while (stack.length) {
const parentID = stack.pop()
if (!parentID) continue
const children = byParent.get(parentID)
if (!children) continue
for (const child of children) {
if (removed.has(child)) continue
removed.add(child)
stack.push(child)
}
}
draft.session = draft.session.filter((s) => !removed.has(s.id))
}),
)
if (session.id === params.id) {
if (nextSession) {
navigate(`/${params.dir}/session/${nextSession.id}`)
} else {
navigate(`/${params.dir}/session`)
}
}
}
command.register(() => {
const commands: CommandOption[] = [
{
@@ -975,11 +1048,16 @@ export default function Layout(props: ParentProps) {
const displayName = (project: LocalProject) => project.name || getFilename(project.worktree)
async function renameProject(project: LocalProject, next: string) {
if (!project.id) return
const current = displayName(project)
if (next === current) return
const name = next === getFilename(project.worktree) ? "" : next
await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
if (project.id && project.id !== "global") {
await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
return
}
globalSync.project.meta(project.worktree, { name })
}
async function renameSession(session: Session, next: string) {
@@ -1125,16 +1203,53 @@ export default function Layout(props: ParentProps) {
setBusy(directory, false)
dismiss()
const href = `/${base64Encode(directory)}/session`
navigate(href)
layout.mobileSidebar.hide()
showToast({
title: language.t("workspace.reset.success.title"),
description: language.t("workspace.reset.success.description"),
actions: [
{
label: language.t("command.session.new"),
onClick: () => {
const href = `/${base64Encode(directory)}/session`
navigate(href)
layout.mobileSidebar.hide()
},
},
{
label: language.t("common.dismiss"),
onClick: "dismiss",
},
],
})
}
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: { directory: string }) {
const name = createMemo(() => getFilename(props.directory))
const [data, setData] = createStore({
@@ -1161,9 +1276,9 @@ export default function Layout(props: ParentProps) {
})
})
const handleDelete = async () => {
await deleteWorkspace(props.directory)
const handleDelete = () => {
dialog.close()
void deleteWorkspace(props.directory)
}
const description = () => {
@@ -1298,6 +1413,11 @@ export default function Layout(props: ParentProps) {
),
)
createEffect(() => {
const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48
document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
})
createEffect(() => {
const project = currentProject()
if (!project) return
@@ -1349,17 +1469,22 @@ export default function Layout(props: ParentProps) {
function workspaceIds(project: LocalProject | undefined) {
if (!project) return []
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
const local = project.worktree
const dirs = [local, ...(project.sandboxes ?? [])]
const active = currentProject()
const directory = active?.worktree === project.worktree && params.dir ? base64Decode(params.dir) : undefined
const next = directory && directory !== project.worktree && !dirs.includes(directory) ? [...dirs, directory] : dirs
const extra = directory && directory !== local && !dirs.includes(directory) ? directory : undefined
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
const existing = store.workspaceOrder[project.worktree]
if (!existing) return next
if (!existing) return extra ? [...dirs, extra] : dirs
const keep = existing.filter((d) => next.includes(d))
const missing = next.filter((d) => !existing.includes(d))
return [...keep, ...missing]
const keep = existing.filter((d) => d !== local && dirs.includes(d))
const missing = dirs.filter((d) => d !== local && !existing.includes(d))
const merged = [local, ...(pending && extra ? [extra] : []), ...missing, ...keep]
if (!extra) return merged
if (pending) return merged
return [...merged, extra]
}
function handleWorkspaceDragStart(event: unknown) {
@@ -1475,6 +1600,8 @@ export default function Layout(props: ParentProps) {
const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened())
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
const isActive = createMemo(() => props.session.id === params.id)
const [menuOpen, setMenuOpen] = createSignal(false)
const [pendingRename, setPendingRename] = createSignal(false)
const messageLabel = (message: Message) => {
const parts = sessionStore.part[message.id] ?? []
@@ -1485,9 +1612,10 @@ export default function Layout(props: ParentProps) {
const item = (
<A
href={`${props.slug}/session/${props.session.id}`}
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] 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] ${menuOpen() ? "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"}`}
onMouseEnter={() => prefetchSession(props.session, "high")}
onFocus={() => prefetchSession(props.session, "high")}
onClick={() => setHoverSession(undefined)}
>
<div class="flex items-center gap-1 w-full">
<div
@@ -1555,46 +1683,115 @@ export default function Layout(props: ParentProps) {
when={hoverReady()}
fallback={<div class="text-12-regular text-text-weak">{language.t("session.messages.loading")}</div>}
>
<MessageNav
messages={hoverMessages() ?? []}
current={undefined}
getLabel={messageLabel}
onMessageSelect={(message) => {
if (!isActive()) {
sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`)
navigate(`${props.slug}/session/${props.session.id}`)
return
}
window.history.replaceState(null, "", `#message-${message.id}`)
window.dispatchEvent(new HashChangeEvent("hashchange"))
}}
size="normal"
class="w-60"
/>
<div class="overflow-y-auto max-h-72 h-full">
<MessageNav
messages={hoverMessages() ?? []}
current={undefined}
getLabel={messageLabel}
onMessageSelect={(message) => {
if (!isActive()) {
sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`)
navigate(`${props.slug}/session/${props.session.id}`)
return
}
window.history.replaceState(null, "", `#message-${message.id}`)
window.dispatchEvent(new HashChangeEvent("hashchange"))
}}
size="normal"
class="w-60"
/>
</div>
</Show>
</HoverCard>
</Show>
<div
class={`hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"}`}
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
classList={{
"opacity-100 pointer-events-auto": menuOpen(),
"opacity-0 pointer-events-none": !menuOpen(),
"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,
}}
>
<TooltipKeybind
placement={props.mobile ? "bottom" : "right"}
title={language.t("command.session.archive")}
keybind={command.keybind("session.archive")}
gutter={8}
>
<IconButton
icon="archive"
variant="ghost"
onClick={() => archiveSession(props.session)}
aria-label={language.t("command.session.archive")}
/>
</TooltipKeybind>
<DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}>
<Tooltip value={language.t("common.moreOptions")} placement="top">
<DropdownMenu.Trigger
as={IconButton}
icon="dot-grid"
variant="ghost"
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
aria-label={language.t("common.moreOptions")}
/>
</Tooltip>
<DropdownMenu.Portal>
<DropdownMenu.Content
onCloseAutoFocus={(event) => {
if (!pendingRename()) return
event.preventDefault()
setPendingRename(false)
openEditor(`session:${props.session.id}`, props.session.title)
}}
>
<DropdownMenu.Item
onSelect={() => {
setPendingRename(true)
setMenuOpen(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>
)
}
const NewSessionItem = (props: { slug: string; mobile?: boolean; dense?: boolean }): JSX.Element => {
const label = language.t("command.session.new")
const tooltip = () => props.mobile || !layout.sidebar.opened()
const item = (
<A
href={`${props.slug}/session`}
end
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
onClick={() => setHoverSession(undefined)}
>
<div class="flex items-center gap-1 w-full">
<div class="shrink-0 size-6 flex items-center justify-center">
<Icon name="plus-small" size="small" class="text-icon-weak" />
</div>
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
{label}
</span>
</div>
</A>
)
return (
<div class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3 hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active">
<Show
when={!tooltip()}
fallback={
<Tooltip placement={props.mobile ? "bottom" : "right"} value={label} gutter={10}>
{item}
</Tooltip>
}
>
{item}
</Show>
</div>
)
}
const SessionSkeleton = (props: { count?: number }): JSX.Element => {
const items = Array.from({ length: props.count ?? 4 }, (_, index) => index)
return (
@@ -1781,9 +1978,6 @@ export default function Layout(props: ParentProps) {
openEditor(`workspace:${props.directory}`, workspaceValue())
}}
>
<DropdownMenu.Item onSelect={() => navigate(`/${slug()}/session`)}>
<DropdownMenu.ItemLabel>{language.t("command.session.new")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
disabled={local()}
onSelect={() => {
@@ -1808,19 +2002,6 @@ export default function Layout(props: ParentProps) {
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<TooltipKeybind
placement="right"
title={language.t("command.session.new")}
keybind={command.keybind("session.new")}
>
<IconButton
icon="plus-small"
variant="ghost"
class="size-6 rounded-md"
onClick={() => navigate(`/${slug()}/session`)}
aria-label={language.t("command.session.new")}
/>
</TooltipKeybind>
</div>
</div>
</div>
@@ -1828,16 +2009,9 @@ export default function Layout(props: ParentProps) {
<Collapsible.Content>
<nav class="flex flex-col gap-1 px-2">
<Button
as={A}
href={`${slug()}/session`}
variant="ghost"
size="large"
icon="edit"
class="hidden _flex w-full text-left justify-start text-text-base rounded-md px-3"
>
{language.t("command.session.new")}
</Button>
<Show when={workspaceSetting()}>
<NewSessionItem slug={slug()} mobile={props.mobile} />
</Show>
<Show when={loading()}>
<SessionSkeleton />
</Show>
@@ -1874,7 +2048,9 @@ export default function Layout(props: ParentProps) {
})
const workspaces = createMemo(() => workspaceIds(props.project).slice(0, 2))
const workspaceEnabled = createMemo(() => layout.sidebar.workspaces(props.project.worktree)())
const workspaceEnabled = createMemo(
() => props.project.vcs === "git" && layout.sidebar.workspaces(props.project.worktree)(),
)
const [open, setOpen] = createSignal(false)
const label = (directory: string) => {
@@ -1916,6 +2092,7 @@ export default function Layout(props: ParentProps) {
"bg-surface-base-hover border border-border-weak-base": !selected() && open(),
}}
onClick={() => navigateToProject(props.project.worktree)}
onBlur={() => setOpen(false)}
>
<ProjectIcon project={props.project} notify />
</button>
@@ -1937,7 +2114,22 @@ export default function Layout(props: ParentProps) {
}}
>
<div class="-m-3 p-2 flex flex-col w-72">
<div class="px-4 pt-2 pb-1 text-14-medium text-text-strong truncate">{displayName(props.project)}</div>
<div class="px-4 pt-2 pb-1 flex items-center gap-2">
<div class="text-14-medium text-text-strong truncate grow">{displayName(props.project)}</div>
<Tooltip value={language.t("common.close")} placement="top" gutter={6}>
<IconButton
icon="circle-x"
variant="ghost"
class="shrink-0"
aria-label={language.t("common.close")}
onClick={(event) => {
event.stopPropagation()
setOpen(false)
closeProject(props.project.worktree)
}}
/>
</Tooltip>
</div>
<div class="px-4 pb-2 text-12-medium text-text-weak">{language.t("sidebar.project.recentSessions")}</div>
<div class="px-2 pb-2 flex flex-col gap-2">
<Show
@@ -1986,11 +2178,11 @@ export default function Layout(props: ParentProps) {
variant="ghost"
class="flex w-full text-left justify-start text-text-base px-2 hover:bg-transparent active:bg-transparent"
onClick={() => {
layout.sidebar.open()
setOpen(false)
if (selected()) {
setOpen(false)
return
}
layout.sidebar.open()
navigateToProject(props.project.worktree)
}}
>
@@ -2028,6 +2220,9 @@ export default function Layout(props: ParentProps) {
style={{ "overflow-anchor": "none" }}
>
<nav class="flex flex-col gap-1 px-2">
<Show when={workspaceSetting()}>
<NewSessionItem slug={slug()} mobile={props.mobile} />
</Show>
<Show when={loading()}>
<SessionSkeleton />
</Show>
@@ -2084,8 +2279,29 @@ export default function Layout(props: ParentProps) {
if (!created?.directory) return
const local = current.worktree
const key = workspaceKey(created.directory)
const root = workspaceKey(local)
setBusy(created.directory, true)
WorktreeState.pending(created.directory)
setStore("workspaceExpanded", key, true)
if (key !== created.directory) {
setStore("workspaceExpanded", created.directory, true)
}
setStore("workspaceOrder", current.worktree, (prev) => {
const existing = prev ?? []
const next = existing.filter((item) => {
const id = workspaceKey(item)
if (id === root) return false
return id !== key
})
return [local, created.directory, ...next]
})
globalSync.child(created.directory)
navigate(`/${base64Encode(created.directory)}/session`)
layout.mobileSidebar.hide()
}
command.register(() => [
@@ -2094,7 +2310,7 @@ export default function Layout(props: ParentProps) {
title: language.t("workspace.new"),
category: language.t("command.category.workspace"),
keybind: "mod+shift+w",
disabled: !layout.sidebar.workspaces(project()?.worktree ?? "")(),
disabled: !workspaceSetting(),
onSelect: createWorkspace,
},
])
@@ -2194,7 +2410,7 @@ export default function Layout(props: ParentProps) {
/>
<Tooltip
placement={sidebarProps.mobile ? "bottom" : "top"}
placement="bottom"
gutter={2}
value={project()?.worktree}
class="shrink-0"
@@ -2222,7 +2438,18 @@ export default function Layout(props: ParentProps) {
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p} />)}>
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => layout.sidebar.toggleWorkspaces(p.worktree)}>
<DropdownMenu.Item
disabled={p.vcs !== "git" && !layout.sidebar.workspaces(p.worktree)()}
onSelect={() => {
const enabled = layout.sidebar.workspaces(p.worktree)()
if (enabled) {
layout.sidebar.toggleWorkspaces(p.worktree)
return
}
if (p.vcs !== "git") return
layout.sidebar.toggleWorkspaces(p.worktree)
}}
>
<DropdownMenu.ItemLabel>
{layout.sidebar.workspaces(p.worktree)()
? language.t("sidebar.workspaces.disable")
@@ -2240,7 +2467,7 @@ export default function Layout(props: ParentProps) {
</div>
<Show
when={layout.sidebar.workspaces(p.worktree)()}
when={workspaceSetting()}
fallback={
<>
<div class="py-4 px-3">
@@ -2343,7 +2570,8 @@ export default function Layout(props: ParentProps) {
<div class="relative bg-background-base flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
<Titlebar />
<div class="flex-1 min-h-0 flex">
<div
<nav
aria-label={language.t("sidebar.nav.projectsAndSessions")}
classList={{
"hidden xl:block": true,
"relative shrink-0": true,
@@ -2364,7 +2592,7 @@ export default function Layout(props: ParentProps) {
onCollapse={layout.sidebar.close}
/>
</Show>
</div>
</nav>
<div class="xl:hidden">
<div
classList={{
@@ -2376,7 +2604,8 @@ export default function Layout(props: ParentProps) {
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
}}
/>
<div
<nav
aria-label={language.t("sidebar.nav.projectsAndSessions")}
classList={{
"@container fixed top-10 bottom-0 left-0 z-50 w-72 bg-background-base transition-transform duration-200 ease-out": true,
"translate-x-0": layout.mobileSidebar.opened(),
@@ -2385,7 +2614,7 @@ export default function Layout(props: ParentProps) {
onClick={(e) => e.stopPropagation()}
>
<SidebarContent mobile />
</div>
</nav>
</div>
<main
@@ -2400,6 +2629,7 @@ export default function Layout(props: ParentProps) {
</main>
</div>
<Toast.Region />
<ReleaseNotesHandler />
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -106,5 +106,12 @@ export function soundSrc(id: string | undefined) {
export function playSound(src: string | undefined) {
if (typeof Audio === "undefined") return
if (!src) return
void new Audio(src).play().catch(() => undefined)
const audio = new Audio(src)
audio.play().catch(() => undefined)
// Return a cleanup function to pause the sound.
return () => {
audio.pause()
audio.currentTime = 0
}
}

View File

@@ -0,0 +1,58 @@
const normalize = (directory: string) => directory.replace(/[\\/]+$/, "")
type State =
| {
status: "pending"
}
| {
status: "ready"
}
| {
status: "failed"
message: string
}
const state = new Map<string, State>()
const waiters = new Map<string, Array<(state: State) => void>>()
export const Worktree = {
get(directory: string) {
return state.get(normalize(directory))
},
pending(directory: string) {
const key = normalize(directory)
const current = state.get(key)
if (current && current.status !== "pending") return
state.set(key, { status: "pending" })
},
ready(directory: string) {
const key = normalize(directory)
state.set(key, { status: "ready" })
const list = waiters.get(key)
if (!list) return
waiters.delete(key)
for (const fn of list) fn({ status: "ready" })
},
failed(directory: string, message: string) {
const key = normalize(directory)
state.set(key, { status: "failed", message })
const list = waiters.get(key)
if (!list) return
waiters.delete(key)
for (const fn of list) fn({ status: "failed", message })
},
wait(directory: string) {
const key = normalize(directory)
const current = state.get(key)
if (current && current.status !== "pending") return Promise.resolve(current)
return new Promise<State>((resolve) => {
const list = waiters.get(key)
if (!list) {
waiters.set(key, [resolve])
return
}
list.push(resolve)
})
},
}

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