Compare commits

...

99 Commits

Author SHA1 Message Date
opencode a7774115c5 release: v1.0.62 2025-11-13 00:13:18 +00:00
Luke Parker 288bc88e40 fix: Tool calling on windows (#4234) 2025-11-12 17:47:39 -06:00
Aiden Cline 6d36dbf9de fix: github action dirty check (#4262) 2025-11-12 16:16:07 -06:00
OpeOginni 4ab4baf3a4 feat(sidebar): add expandable sections for sidebar (#4132)
Co-authored-by: GitHub Action <action@github.com>
2025-11-12 16:15:17 -06:00
phantomreactor 90f05eb9c2 paste images in wsl using ctrl+v (#4123)
Co-authored-by: GitHub Action <action@github.com>
2025-11-12 15:10:23 -06:00
Melih Mucuk b63b6d04c6 Fix usage & billing for custom model aliases and cached/reasoning tokens (#4222)
Co-authored-by: Melih Mucuk <melih@monkeysteam.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-12 13:59:35 -06:00
Aiden Cline 8addaa7e08 fix: custom model name merging 2025-11-12 13:55:13 -06:00
Elias a96bf8e62d docs: OVHcloud AI Endpoints provider (#4257) 2025-11-12 13:28:35 -06:00
Ivan c8bda598f5 fix: correct cache cost for OpenRouter and other OpenAI-compatible providers (#4256) 2025-11-12 12:41:44 -06:00
Adam c857cff585 fix(desktop): double listing dir 2025-11-12 12:17:54 -06:00
Aiden Cline fd9d2db755 ci: update zed sync 2025-11-12 10:52:20 -06:00
Aiden Cline b19fd14f80 ignore: make issue button send opencode version too 2025-11-12 10:40:48 -06:00
Sebastian Herrlinger a0f469095c upgrade opentui to 0.1.42, fixing some CJK/grapheme issues with prompt extmarks and char corruption 2025-11-12 15:35:16 +01:00
Adam 0ccb26df94 feat(desktop): sticky diff headers 2025-11-12 07:03:39 -06:00
Adam 71fd5966ad fix(desktop): styling tweaks 2025-11-12 07:03:38 -06:00
GitHub Action c02230de4f ignore: update download stats 2025-11-12 2025-11-12 12:05:15 +00:00
Filip aa2e2c76c0 fix: clangd hanging fixed (#3611)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
2025-11-12 00:21:55 -06:00
opencode 7c2d4ee79a release: v1.0.61 2025-11-12 03:10:55 +00:00
Dax Raad e3a2728fa3 tui: add double-esc interrupt mechanism for long-running operations
Users can now press escape twice within 5 seconds to interrupt long-running
operations in the TUI. The first press shows a visual hint, and the second
press aborts the current session.
2025-11-11 22:04:00 -05:00
Boston Cartwright 18260b037b feat: add SourceKit LSP support (#1545)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-11 20:51:33 -06:00
Dax Raad ad83dd3ad9 tui: fix autocomplete display to prevent long file paths from breaking layout 2025-11-12 02:36:43 +00:00
opencode 6f37315cd1 release: v1.0.60 2025-11-12 02:36:42 +00:00
Dax d81dce6a82 fix: add support for loading custom themes from .opencode/themes directory (#4229)
Co-authored-by: GitHub Action <action@github.com>
2025-11-11 21:30:38 -05:00
opencode 0bd11e970b release: v1.0.59 2025-11-12 02:07:41 +00:00
Dax Raad 7e29e1dd23 better errors on initial tui boot 2025-11-11 21:01:45 -05:00
Rafał Krzyważnia 491a2adf8d fix: resolve @file references in slash commands with subagents (#4221) 2025-11-11 19:38:50 -06:00
Aiden Cline c07d6487a8 fix config ordering (#4228) 2025-11-11 19:27:34 -06:00
Aiden Cline 9990e84d37 fix: ensure revert dialog moves that prompt to input box (#4227) 2025-11-11 19:08:59 -06:00
Aiden Cline 0b86adbe99 feat: agent color cfg (#4226)
Co-authored-by: 0xrin <0xrin1@protonmail.com>
Co-authored-by: GitHub Action <action@github.com>
2025-11-11 18:32:44 -06:00
Frank 834a2c09d5 wip: poc pr command 2025-11-11 18:50:28 -05:00
Frank f13c17e654 wip: poc pr command 2025-11-11 18:50:28 -05:00
Julian LaNeve a0611d92e4 docs: Update config references to latest Sonnet & Haiku models (#4210) 2025-11-11 16:52:45 -06:00
Aiden Cline 0b001c3e80 tweak: make todos appear list of modified files 2025-11-11 16:05:23 -06:00
Sebastian Herrlinger 53b7cb62c4 upgrade opentui to 0.1.41:
- enables modifyOtherKeys to get CSI u sequences in terminals that support it
- uses Private Mode 2026 for synced rendering to fix cursor flickering in terminals like iTerm2
- lazy highlighting for code renderables (perf)
- linear scroll acceleration by default
- align textarea default bindings more with readline
- fix vertical cursor movement in textarea
- introduce stdin buffer to handle chunked sequences
- improve capability detection (async)
- renderer emits focus/blur events when app is focused/blurred (if supported by terminal)
2025-11-11 23:00:31 +01:00
Aiden Cline c5e096c76a fix: costs being 0 when using custom model id overrides (#4219) 2025-11-11 15:58:14 -06:00
Aiden Cline e1fc4a756b Hide /share if disabled (#4215) 2025-11-11 14:47:39 -06:00
Aiden Cline e5bc4cbbcf ci: update changelog script 2025-11-11 14:27:13 -06:00
GitHub Action 459d5ec19b chore: format code 2025-11-11 20:21:00 +00:00
Aiden Cline 8baa222621 ci: update script 2025-11-11 14:20:19 -06:00
Dax Raad ce1397cc34 core: add test to verify OpenCode doesn't crash when starting in git repositories with no commit history 2025-11-11 20:17:36 +00:00
Ron Suhodrev dc7c5ced4c tui: restore full text when editing prompts with summarized content (#4030) 2025-11-11 20:17:36 +00:00
Corwin Marsh b8e8fe7e31 docs: Update dead Context7 mcp server link (#4207)
Co-authored-by: Corwin Marsh <corwinm@users.noreply.github.com>
2025-11-11 20:17:36 +00:00
opencode 890085758f release: v1.0.58 2025-11-11 20:17:36 +00:00
Dax Raad 85f15893bc core: prevent crash when starting in repositories without any commits yet 2025-11-11 15:11:42 -05:00
Adam 98be75b17c fix(desktop): give review pane more width 2025-11-11 13:02:59 -06:00
GitHub Action b5cc27b8ea chore: format code 2025-11-11 18:38:23 +00:00
Frank 05937b52cc chore: format code 2025-11-11 13:37:36 -05:00
GitHub Action 62b82570e1 chore: format code 2025-11-11 17:34:09 +00:00
Dax Raad 4bf75c0b44 core: remove unused experimental flags for turn summary and no-bootstrap to simplify feature flag management 2025-11-11 12:33:26 -05:00
opencode a8a06c4983 release: v1.0.57 2025-11-11 17:30:26 +00:00
Dax Raad b0b7fd143b tui: show LSP diagnostics inline when viewing files so users can see type errors and compilation issues without leaving the interface 2025-11-11 12:15:40 -05:00
GitHub Action 140498eb4f chore: format code 2025-11-11 16:59:37 +00:00
Haris Gušić ca5126e24d fix: TUI spawn: reset BUN_OPTIONS (#3606) 2025-11-11 10:58:59 -06:00
Josiah Witt fb2b3e567c docs: update keymap.json bindings for OpenCode command (#4192) 2025-11-11 10:48:10 -06:00
Adam c672a1963b fix(desktop): prompt clearing inconsistent 2025-11-11 09:35:08 -06:00
Adam 54bff6b120 fix(desktop): code/diff number container width 2025-11-11 09:22:35 -06:00
Adam ab3f198fab fix(desktop): session show more hidden on new session 2025-11-11 09:11:34 -06:00
Adam 0057ef6336 fix(desktop): prompt input not clearing, attachments flaky 2025-11-11 09:01:28 -06:00
Adam 4f604b3839 fix(desktop): color grouping 2025-11-11 09:01:27 -06:00
GitHub Action a20489584e ignore: update download stats 2025-11-11 2025-11-11 12:04:42 +00:00
Dax Raad a6b066bd47 ci 2025-11-11 02:15:33 -05:00
Dax Raad 37fdcac05a ci 2025-11-11 02:13:26 -05:00
Dax Raad 299bf1dca8 ci 2025-11-11 01:59:10 -05:00
Dax Raad d685aa38ef type checks 2025-11-11 01:56:01 -05:00
Dax Raad 995b23787c ci 2025-11-11 01:48:29 -05:00
Dax Raad ed8e663e13 ignore 2025-11-11 01:41:58 -05:00
Dax Raad 38cee3b848 ci: sync 2025-11-11 01:37:10 -05:00
Dax Raad 6d116d4b54 ci: fix 2025-11-11 01:35:50 -05:00
Aiden Cline 7c4f111b34 ignore: run bun i 2025-11-11 00:34:09 -06:00
Dax Raad f2fac29270 ci 2025-11-11 01:33:02 -05:00
Dax Raad 12892f0e12 ci: improve bun caching to invalidate when bun version changes in package.json 2025-11-11 01:31:24 -05:00
GitHub Action 9714a3558e chore: format code 2025-11-11 06:28:19 +00:00
Dax Raad e49a1d1f39 ci: fix 2025-11-11 01:27:39 -05:00
Dax Raad 528565510d sync 2025-11-11 01:25:39 -05:00
GitHub Action 36cfda933d chore: format code 2025-11-11 06:24:58 +00:00
Dax Raad ecf5040966 tui: update @opentui/core to v0.1.39 and fix build script for new target format 2025-11-11 01:24:17 -05:00
Frank 7d56603c26 zen: failover on error 2025-11-11 00:29:44 -05:00
Aiden Cline 02b7cc8313 keep session dot in list for current active (#4185) 2025-11-10 22:20:35 -06:00
Dax Raad c9a52c9a85 cache project id in root git folder 2025-11-10 21:57:55 -05:00
Dax Raad dea668b0ea tui: help users read thinking blocks and trust todo syncs 2025-11-10 20:34:04 -05:00
Aiden Cline 1bc3e92376 fix: undefined check 2025-11-10 19:21:57 -06:00
Dax Raad 3f5acc3dff add web and codesearch tools 2025-11-10 16:39:54 -05:00
Aiden Cline 0588011476 ignore: bump copilot plugin version 2025-11-10 13:40:15 -06:00
OpeOginni bba72c82ae Fix/google vertex configs (#4169)
Co-authored-by: GitHub Action <action@github.com>
2025-11-10 13:25:03 -06:00
denesbeck e95181a551 Refactor/redundant toast comp (#4163)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-10 11:27:19 -06:00
GitHub Action 74e8c2e50f chore: format code 2025-11-10 17:19:20 +00:00
David Hill cdabafa264 wip code theme inc light 2025-11-10 17:18:37 +00:00
denesbeck 0a92af60a0 fix: upgrade toast notification (#4159) 2025-11-10 10:28:28 -06:00
David Hill c7808a4b01 wip code theme 2025-11-10 16:16:50 +00:00
David Hill 7f978e07ff wip code theme 2025-11-10 16:14:24 +00:00
David Hill a4ae1bb9eb wip code theme 2025-11-10 16:00:05 +00:00
David Hill 96a39803cc wip code theme 2025-11-10 15:58:56 +00:00
GitHub Action 16f8f20b31 chore: format code 2025-11-10 14:51:24 +00:00
David Hill 06b1684ddb wip code editor update dark mode 2025-11-10 14:50:36 +00:00
David Hill c6e830c954 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-11-10 13:44:12 +00:00
GitHub Action fc78c28df6 ignore: update download stats 2025-11-10 2025-11-10 12:04:53 +00:00
David Hill 7088bfabd7 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-11-04 21:36:46 +00:00
David Hill dbdbfb8543 Update button.css 2025-11-04 17:31:56 +00:00
David Hill 521803aaa3 Update theme.css 2025-11-04 17:31:51 +00:00
100 changed files with 2212 additions and 2333 deletions
+2 -2
View File
@@ -13,9 +13,9 @@ runs:
uses: actions/cache@v4
with:
path: ~/.bun
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb', 'bun.lock') }}
key: ${{ runner.os }}-bun-${{ hashFiles('package.json') }}-${{ hashFiles('bun.lockb', 'bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
${{ runner.os }}-bun-${{ hashFiles('package.json') }}-
- name: Install dependencies
run: bun install
+1 -14
View File
@@ -1,17 +1,4 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-openai-codex-auth"],
"mcp": {
"weather": {
"type": "local",
"command": ["bun", "x", "@h1deya/mcp-server-weather"]
},
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/mcp",
"headers": {
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
}
}
}
"plugin": ["opencode-openai-codex-auth"]
}
+223
View File
@@ -0,0 +1,223 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"nord0": "#2E3440",
"nord1": "#3B4252",
"nord2": "#434C5E",
"nord3": "#4C566A",
"nord4": "#D8DEE9",
"nord5": "#E5E9F0",
"nord6": "#ECEFF4",
"nord7": "#8FBCBB",
"nord8": "#88C0D0",
"nord9": "#81A1C1",
"nord10": "#5E81AC",
"nord11": "#BF616A",
"nord12": "#D08770",
"nord13": "#EBCB8B",
"nord14": "#A3BE8C",
"nord15": "#B48EAD"
},
"theme": {
"primary": {
"dark": "nord8",
"light": "nord10"
},
"secondary": {
"dark": "nord9",
"light": "nord9"
},
"accent": {
"dark": "nord7",
"light": "nord7"
},
"error": {
"dark": "nord11",
"light": "nord11"
},
"warning": {
"dark": "nord12",
"light": "nord12"
},
"success": {
"dark": "nord14",
"light": "nord14"
},
"info": {
"dark": "nord8",
"light": "nord10"
},
"text": {
"dark": "nord4",
"light": "nord0"
},
"textMuted": {
"dark": "nord3",
"light": "nord1"
},
"background": {
"dark": "nord0",
"light": "nord6"
},
"backgroundPanel": {
"dark": "nord1",
"light": "nord5"
},
"backgroundElement": {
"dark": "nord1",
"light": "nord4"
},
"border": {
"dark": "nord2",
"light": "nord3"
},
"borderActive": {
"dark": "nord3",
"light": "nord2"
},
"borderSubtle": {
"dark": "nord2",
"light": "nord3"
},
"diffAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffContext": {
"dark": "nord3",
"light": "nord3"
},
"diffHunkHeader": {
"dark": "nord3",
"light": "nord3"
},
"diffHighlightAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffHighlightRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffAddedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffContextBg": {
"dark": "nord1",
"light": "nord5"
},
"diffLineNumber": {
"dark": "nord2",
"light": "nord4"
},
"diffAddedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"markdownText": {
"dark": "nord4",
"light": "nord0"
},
"markdownHeading": {
"dark": "nord8",
"light": "nord10"
},
"markdownLink": {
"dark": "nord9",
"light": "nord9"
},
"markdownLinkText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCode": {
"dark": "nord14",
"light": "nord14"
},
"markdownBlockQuote": {
"dark": "nord3",
"light": "nord3"
},
"markdownEmph": {
"dark": "nord12",
"light": "nord12"
},
"markdownStrong": {
"dark": "nord13",
"light": "nord13"
},
"markdownHorizontalRule": {
"dark": "nord3",
"light": "nord3"
},
"markdownListItem": {
"dark": "nord8",
"light": "nord10"
},
"markdownListEnumeration": {
"dark": "nord7",
"light": "nord7"
},
"markdownImage": {
"dark": "nord9",
"light": "nord9"
},
"markdownImageText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCodeBlock": {
"dark": "nord4",
"light": "nord0"
},
"syntaxComment": {
"dark": "nord3",
"light": "nord3"
},
"syntaxKeyword": {
"dark": "nord9",
"light": "nord9"
},
"syntaxFunction": {
"dark": "nord8",
"light": "nord8"
},
"syntaxVariable": {
"dark": "nord7",
"light": "nord7"
},
"syntaxString": {
"dark": "nord14",
"light": "nord14"
},
"syntaxNumber": {
"dark": "nord15",
"light": "nord15"
},
"syntaxType": {
"dark": "nord7",
"light": "nord7"
},
"syntaxOperator": {
"dark": "nord9",
"light": "nord9"
},
"syntaxPunctuation": {
"dark": "nord4",
"light": "nord0"
}
}
}
+3
View File
@@ -135,3 +135,6 @@
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
+103 -100
View File
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "opencode",
@@ -39,7 +40,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.55",
"version": "1.0.62",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -66,7 +67,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.55",
"version": "1.0.62",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -90,7 +91,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.55",
"version": "1.0.62",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -114,7 +115,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.55",
"version": "1.0.62",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -154,7 +155,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.55",
"version": "1.0.62",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -170,7 +171,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.55",
"version": "1.0.62",
"bin": {
"opencode": "./bin/opencode",
},
@@ -188,8 +189,8 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.0.0-20251108-0c7899b1",
"@opentui/solid": "0.0.0-20251108-0c7899b1",
"@opentui/core": "0.1.42",
"@opentui/solid": "0.1.42",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -248,7 +249,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.55",
"version": "1.0.62",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -268,7 +269,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.55",
"version": "1.0.62",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -279,7 +280,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.55",
"version": "1.0.62",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -292,7 +293,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.55",
"version": "1.0.62",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -322,7 +323,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.55",
"version": "1.0.62",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -437,7 +438,7 @@
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.2.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", "shiki": "^3.0.0", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg=="],
"@astrojs/mdx": ["@astrojs/mdx@4.3.9", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.8", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "picocolors": "^1.1.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-80LHiM4z3FxAjATHNgFpa8nlTNSprAWB4UUKnr/QG56Pwk7uRnJWrXlok4wSCi/3fg8kTZ98A408Q91M+iqJdw=="],
"@astrojs/mdx": ["@astrojs/mdx@4.3.10", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.8", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "picocolors": "^1.1.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-2T5+XIr7PMqMeXhRofXY5NlY4lA0Km+wkfsqmr9lq5KXUHpGlKPQ9dlDZJP9E/CtljJyEBNS17zq66LrIJ1tiQ=="],
"@astrojs/prism": ["@astrojs/prism@3.2.0", "", { "dependencies": { "prismjs": "^1.29.0" } }, "sha512-GilTHKGCW6HMq7y3BUv9Ac7GMe/MO9gi9GW62GzKtth0SwukCu/qp2wLiGpEujhY+VVhaG9v7kv/5vFzvf4NYw=="],
@@ -577,15 +578,15 @@
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.9", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20250927.0" }, "optionalPeers": ["workerd"] }, "sha512-Drm7qlTKnvncEv+DANiQNEonq0H0LyIsoFZYJ6tJ8OhAoy5udIE8yp6BsVDYcIjcYLIybp4M7c/P7ly/56SoHg=="],
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251011.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0DirVP+Z82RtZLlK2B+VhLOkk+ShBqDYO/jhcRw4oVlp0TOvk3cOVZChrt3+y3NV8Y/PYgTEywzLKFSziK4wCg=="],
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251105.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-nztUP35wTtUKM+681dBWtUNSySNWELTV+LY43oWy7ZhK19/iBJPQoFY7xpvF7zy4qOOShtise259B65DS4/71Q=="],
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251011.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1WuFBGwZd15p4xssGN/48OE2oqokIuc51YvHvyNivyV8IYnAs3G9bJNGWth1X7iMDPe4g44pZrKhRnISS2+5dA=="],
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251105.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WS/dvPYTW/+gs8s0UvDqDY7wcuIAg/hUpjrMNGepr+Mo38vMU39FYhJQOly99oJCXxMluQqAnRKg09b/9Gr+Rg=="],
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251011.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BccMiBzFlWZyFghIw2szanmYJrJGBGHomw2y/GV6pYXChFzMGZkeCEMfmCyJj29xczZXxcZmUVJxNy4eJxO8QA=="],
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251105.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RdHRHo/hpjR6sNw529FkmslVSz/K3Pb1+i3fIoqUrHCrZOUYzFyz3nLeZh4EYaAhcztLWiSTwBv54bcl4sG3wA=="],
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251011.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-79o/216lsbAbKEVDZYXR24ivEIE2ysDL9jvo0rDTkViLWju9dAp3CpyetglpJatbSi3uWBPKZBEOqN68zIjVsQ=="],
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251105.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-5zkxQCqLjwrqZVVJh92J2Drv6xifkP8kN2ltjHdwZQlVzfDW48d7tAtCm1ZooUv204ixvZFarusCfL+IRjExZg=="],
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251011.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RIXUQRchFdqEvaUqn1cXZXSKjpqMaSaVAkI5jNZ8XzAw/bw2bcdOVUtakrflgxDprltjFb0PTNtuss1FKtH9Jg=="],
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251105.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6BpkfjBIbGR+4FBOcZGcWDLM0XQuoI6R9Dublj/BKf4pv0/xJ4zHdnaYUb5NIlC75L55Ouqw0CEJasoKlMjgnw=="],
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251008.0", "", {}, "sha512-dZLkO4PbCL0qcCSKzuW7KE4GYe49lI12LCfQ5y9XeSwgYBoAUbwH4gmJ6A0qUIURiTJTkGkRkhVPqpq2XNgYRA=="],
@@ -965,21 +966,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.0.0-20251108-0c7899b1", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251108-0c7899b1", "@opentui/core-darwin-x64": "0.0.0-20251108-0c7899b1", "@opentui/core-linux-arm64": "0.0.0-20251108-0c7899b1", "@opentui/core-linux-x64": "0.0.0-20251108-0c7899b1", "@opentui/core-win32-arm64": "0.0.0-20251108-0c7899b1", "@opentui/core-win32-x64": "0.0.0-20251108-0c7899b1", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-uJ7wbVw2v5NnL6g3v72SjPLUwMl2wqOejUEo8t4NeBA8nsboSxggqkrqOYf6OOmCADoAqyFDY7akZMsz6HMZtg=="],
"@opentui/core": ["@opentui/core@0.1.42", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.42", "@opentui/core-darwin-x64": "0.1.42", "@opentui/core-linux-arm64": "0.1.42", "@opentui/core-linux-x64": "0.1.42", "@opentui/core-win32-arm64": "0.1.42", "@opentui/core-win32-x64": "0.1.42", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-oV2xHBB2HaNiGvaV6R0C8GmniNJSsLKop4APq4FrLyCYberc6vZcATSHcA5YT9krdvHbBDOOn9RI2oaVJYRbUQ=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251108-0c7899b1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DS9CmFmZZjwe6PIhz6zhZAsDx11DtyMFDxn8V3On2b8G892aBG6rHYtBBnsM28/1GGEJBTeDQ/jUXPVd6FNJ/g=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.42", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sk5b/kh/y8HUJ7stGA5ydkajJX/z2OiGqSm+wn6XIoqdDavxQaFoQOt1PCuCqaxqZWJcXZ6OmISDVagZPUsPuw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251108-0c7899b1", "", { "os": "darwin", "cpu": "x64" }, "sha512-K4XwdmT6FTShn7EG8AKliPzO5H59R0XUlZi9+kfRVW59IIJtna5wxbu69SkA28dFoWj5i4yDumwoBI+tI7T6vg=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.42", "", { "os": "darwin", "cpu": "x64" }, "sha512-b0FKTw+t/wlJg4u+wTurWzbQe47gExkjguaGSUua0m0vybrkkvbUvmrADr+yivCjxcPAhSZ3lOOVU3uZuWsNqw=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251108-0c7899b1", "", { "os": "linux", "cpu": "arm64" }, "sha512-3JUmxZeSvxV5yU7NEXSecy5Z1/LcVUMy1oWyusZgp96X0CTYAXMrolZt9IJDGO5raeO7JId1UaJmWW0r4DR8TA=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.42", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vy8BrjJpv2f56JAsYmv4PkC+2HsCv8Gh0ErrlIJQ8L4h29oWabS44m0uxFdvjuTDgKpCJzOScsxsy1VGzSd9rw=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251108-0c7899b1", "", { "os": "linux", "cpu": "x64" }, "sha512-i/AQWGyanpPRpk9NK7Ze1tn+d5bqzM9wZFKNB3rd9d2Vbt/ROgBJItG6igz8vzKPKgnlHK4Gw9b5iG5sbjpd+Q=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.42", "", { "os": "linux", "cpu": "x64" }, "sha512-cO+13E1HIAPUdV/DRdKotHFAxsLc+ipbbFKGAuu/msfvywCnnNs86w22yeMg0cEqx7aBocWWT1XfJEHDJLFOqw=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251108-0c7899b1", "", { "os": "win32", "cpu": "arm64" }, "sha512-C7JLWuNN3w2txiVx3demwNwogVi4DQB5ZNHy2b09++kd2m449/RwGPyLcKpuoTzU4s/usYOeY4TxKIAd8cKedQ=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.42", "", { "os": "win32", "cpu": "arm64" }, "sha512-xpLhODjOWh7gMOSrKIldb4v6hR0TGyz6kjckDKwcjUv3LGbLJuSly+3O/zuWWS60dt56G1X4A0OyjWwiGZjc0g=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251108-0c7899b1", "", { "os": "win32", "cpu": "x64" }, "sha512-mpOryp37YaHlTsN70LhiSn9hJJBktbyhlH/eB3N2K7H1ANYQVrekgBJ3rDxlH1GDVtRz6vLS3IDlyK75qNX4pg=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.42", "", { "os": "win32", "cpu": "x64" }, "sha512-pao5XdAln93WWPdsTF+V+HccZ5d1ijSmv0OoBbkjkVbP+tiN41yxNqg/7jzW9IiAakYsvmpKV+3ixi/dlBEvOQ=="],
"@opentui/solid": ["@opentui/solid@0.0.0-20251108-0c7899b1", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251108-0c7899b1", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-tcsYnFGH/KBlQNG0IyZE2bisnm5NwN/w7theuWga3L1zoXqZqA5dQHutAVg4zkq5l/YKULeDI4jBlvz0lzH88A=="],
"@opentui/solid": ["@opentui/solid@0.1.42", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.42", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-4TNlEtatZ4n9TcKPWSF/EoaPaLmZuFVJ4hHh9wRggNaGrmDlmJ+9N/8oEKXETt+oRDX/1CdowAaTOVfaqb1t6g=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -1129,49 +1130,49 @@
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.2", "", { "os": "android", "cpu": "arm" }, "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.5", "", { "os": "android", "cpu": "arm64" }, "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.2", "", { "os": "android", "cpu": "arm64" }, "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.2", "", { "os": "linux", "cpu": "arm" }, "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.2", "", { "os": "linux", "cpu": "arm" }, "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.2", "", { "os": "linux", "cpu": "none" }, "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.2", "", { "os": "linux", "cpu": "none" }, "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.2", "", { "os": "linux", "cpu": "none" }, "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.2", "", { "os": "linux", "cpu": "x64" }, "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.5", "", { "os": "none", "cpu": "arm64" }, "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.2", "", { "os": "none", "cpu": "arm64" }, "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.2", "", { "os": "win32", "cpu": "x64" }, "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.2", "", { "os": "win32", "cpu": "x64" }, "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA=="],
"@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="],
@@ -1207,57 +1208,57 @@
"@slack/web-api": ["@slack/web-api@6.13.0", "", { "dependencies": { "@slack/logger": "^3.0.0", "@slack/types": "^2.11.0", "@types/is-stream": "^1.1.0", "@types/node": ">=12.0.0", "axios": "^1.7.4", "eventemitter3": "^3.1.0", "form-data": "^2.5.0", "is-electron": "2.2.2", "is-stream": "^1.1.0", "p-queue": "^6.6.1", "p-retry": "^4.0.0" } }, "sha512-dv65crIgdh9ZYHrevLU6XFHTQwTyDmNqEqzuIrV+Vqe/vgiG6w37oex5ePDU1RGm2IJ90H8iOvHFvzdEO/vB+g=="],
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ=="],
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA=="],
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.2", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-4Jys0ni2tB2VZzgslbEgszZyMdTkPOFGA8g+So/NjR8oy6Qwaq4eSwsrRI+NMtb0Dq4kqCzGUu/nGUx7OM/xfw=="],
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw=="],
"@smithy/core": ["@smithy/core@3.17.2", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-stream": "^4.5.5", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ=="],
"@smithy/core": ["@smithy/core@3.18.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-vGSDXOJFZgOPTatSI1ly7Gwyy/d/R9zh2TO3y0JZ0uut5qQ88p9IaWaZYIWSSqtdekNM4CGok/JppxbAff4KcQ=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/property-provider": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-YVNMjhdz2pVto5bRdux7GMs0x1m0Afz3OcQy/4Yf9DH4fWOtroGH7uLvs7ZmDyoBJzLdegtIPpXrpJOZWvUXdw=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg=="],
"@smithy/hash-node": ["@smithy/hash-node@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw=="],
"@smithy/hash-node": ["@smithy/hash-node@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA=="],
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-z6aDLGiHzsMhbS2MjetlIWopWz//K+mCoPXjW6aLr0mypF+Y7qdEh5TyJ20Onf9FbWHiWl4eC+rITdizpnXqOw=="],
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A=="],
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="],
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-hJRZuFS9UsElX4DJSJfoX4M1qXRH+VFiLMUnhsWvtOOUWRNvvOfDaUSdlNbjwv1IkpVjj/Rd/O59Jl3nhAcxow=="],
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A=="],
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.6", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-serde": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w=="],
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.7", "", { "dependencies": { "@smithy/core": "^3.18.0", "@smithy/middleware-serde": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-i8Mi8OuY6Yi82Foe3iu7/yhBj1HBRoOQwBSsUNYglJTNSFaWYTNM2NauBBs/7pq2sqkLRqeUXA3Ogi2utzpUlQ=="],
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/protocol-http": "^5.3.4", "@smithy/service-error-classification": "^4.2.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-OhLx131znrEDxZPAvH/OYufR9d1nB2CQADyYFN4C3V/NQS7Mg4V6uvxHC/Dr96ZQW8IlHJTJ+vAhKt6oxWRndA=="],
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/service-error-classification": "^4.2.5", "@smithy/smithy-client": "^4.9.3", "@smithy/types": "^4.9.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-E7Vc6WHCHlzDRTx1W0jZ6J1L6ziEV0PIWcUdmfL4y+c8r7WYr6I+LkQudaD8Nfb7C5c4P3SQ972OmXHtv6m/OA=="],
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="],
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-La1ldWTJTZ5NqQyPqnCNeH9B+zjFhrNoQIL1jTh4zuqXRlmXhxYHhMtI1/92OlnoAtp6JoN7kzuwhWoXrBwPqg=="],
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA=="],
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ=="],
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="],
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.5", "", { "dependencies": { "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.5", "", { "dependencies": { "@smithy/abort-controller": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw=="],
"@smithy/property-provider": ["@smithy/property-provider@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w=="],
"@smithy/property-provider": ["@smithy/property-provider@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg=="],
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="],
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ=="],
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="],
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg=="],
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-aHb5cqXZocdzEkZ/CvhVjdw5l4r1aU/9iMEyoKzH4eXMowT6M0YjBpp7W/+XjkBnY8Xh0kVd55GKjnPKlCwinQ=="],
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ=="],
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1" } }, "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng=="],
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0" } }, "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ=="],
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA=="],
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA=="],
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="],
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.5", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w=="],
"@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="],
"@smithy/smithy-client": ["@smithy/smithy-client@4.9.3", "", { "dependencies": { "@smithy/core": "^3.18.0", "@smithy/middleware-endpoint": "^4.3.7", "@smithy/middleware-stack": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-8tlueuTgV5n7inQCkhyptrB3jo2AO80uGrps/XTYZivv5MFQKKBj3CIWIGMI2fRY5LEduIiazOhAWdFknY1O9w=="],
"@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="],
"@smithy/types": ["@smithy/types@4.9.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA=="],
"@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="],
"@smithy/url-parser": ["@smithy/url-parser@4.2.5", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ=="],
"@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="],
@@ -1269,19 +1270,19 @@
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="],
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.5", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-GwaGjv/QLuL/QHQaqhf/maM7+MnRFQQs7Bsl6FlaeK6lm6U7mV5AAnVabw68cIoMl5FQFyKK62u7RWRzWL25OQ=="],
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.6", "", { "dependencies": { "@smithy/property-provider": "^4.2.5", "@smithy/smithy-client": "^4.9.3", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-kbpuXbEf2YQ9zEE6eeVnUCQWO0e1BjMnKrXL8rfXgiWA0m8/E0leU4oSNzxP04WfCmW8vjEqaDeXWxwE4tpOjQ=="],
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.8", "", { "dependencies": { "@smithy/config-resolver": "^4.4.2", "@smithy/credential-provider-imds": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/property-provider": "^4.2.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-gIoTf9V/nFSIZ0TtgDNLd+Ws59AJvijmMDYrOozoMHPJaG9cMRdqNO50jZTlbM6ydzQYY8L/mQ4tKSw/TB+s6g=="],
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.9", "", { "dependencies": { "@smithy/config-resolver": "^4.4.3", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/smithy-client": "^4.9.3", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-dgyribrVWN5qE5usYJ0m5M93mVM3L3TyBPZWe1Xl6uZlH2gzfQx3dz+ZCdW93lWqdedJRkOecnvbnoEEXRZ5VQ=="],
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="],
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A=="],
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="],
"@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="],
"@smithy/util-middleware": ["@smithy/util-middleware@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA=="],
"@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="],
"@smithy/util-retry": ["@smithy/util-retry@4.2.5", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg=="],
"@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="],
"@smithy/util-stream": ["@smithy/util-stream@4.5.6", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ=="],
"@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="],
@@ -1321,7 +1322,7 @@
"@solidjs/meta": ["@solidjs/meta@0.29.4", "", { "peerDependencies": { "solid-js": ">=1.8.4" } }, "sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g=="],
"@solidjs/router": ["@solidjs/router@0.15.3", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw=="],
"@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="],
"@solidjs/start": ["@solidjs/start@1.2.0", "", { "dependencies": { "@tanstack/server-functions-plugin": "1.121.21", "@vinxi/plugin-directives": "^0.5.0", "@vinxi/server-components": "^0.5.0", "cookie-es": "^2.0.0", "defu": "^6.1.2", "error-stack-parser": "^2.1.4", "html-to-image": "^1.11.11", "radix3": "^1.1.0", "seroval": "^1.0.2", "seroval-plugins": "^1.0.2", "shiki": "^1.26.1", "source-map-js": "^1.0.2", "terracotta": "^1.0.4", "tinyglobby": "^0.2.2", "vite-plugin-solid": "^2.11.1" }, "peerDependencies": { "vinxi": "^0.5.7" } }, "sha512-SRv1g3R+4sxZnxCBPK1IedtLKsPhPJ7W/Yv4xEHjM4jJGPWi3ed35/yd0D5zhRK0C7zJIkZKbhnR/S3g8JUD5w=="],
@@ -1455,7 +1456,7 @@
"@types/scheduler": ["@types/scheduler@0.26.0", "", {}, "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA=="],
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
"@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
@@ -1587,7 +1588,7 @@
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
"autoprefixer": ["autoprefixer@10.4.22", "", { "dependencies": { "browserslist": "^4.27.0", "caniuse-lite": "^1.0.30001754", "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
@@ -1617,9 +1618,9 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"bare-events": ["bare-events@2.8.1", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ=="],
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
"bare-fs": ["bare-fs@4.5.0", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-GljgCjeupKZJNetTqxKaQArLK10vpmK28or0+RwWjEl5Rk+/xG3wkpmkv+WrcBm3q1BwHKlnhXzR8O37kcvkXQ=="],
"bare-fs": ["bare-fs@4.5.1", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-zGUCsm3yv/ePt2PHNbVxjjn0nNB1MkIaR4wOCxJ2ig5pCf5cCVAYJXVhQg/3OhhJV6DB1ts7Hv0oUaElc2TPQg=="],
"bare-os": ["bare-os@3.6.2", "", {}, "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A=="],
@@ -1669,7 +1670,7 @@
"brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="],
"browserslist": ["browserslist@4.27.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", "electron-to-chromium": "^1.5.238", "node-releases": "^2.0.26", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw=="],
"browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="],
"buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
@@ -1679,7 +1680,7 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-ffi-structs": ["bun-ffi-structs@0.1.0", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-NoRfJ81pgLIHCzw624/2GS2FuxcU0G4SRJww/4PXvheNVUPSIUjkOC6v1/8rk66tJVCb9oR0D6rDNKK0qT5O2Q=="],
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
@@ -1711,7 +1712,7 @@
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001753", "", {}, "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw=="],
"caniuse-lite": ["caniuse-lite@1.0.30001754", "", {}, "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
@@ -1931,7 +1932,7 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"electron-to-chromium": ["electron-to-chromium@1.5.245", "", {}, "sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.250", "", {}, "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw=="],
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
@@ -2029,7 +2030,7 @@
"expressive-code": ["expressive-code@0.41.3", "", { "dependencies": { "@expressive-code/core": "^0.41.3", "@expressive-code/plugin-frames": "^0.41.3", "@expressive-code/plugin-shiki": "^0.41.3", "@expressive-code/plugin-text-markers": "^0.41.3" } }, "sha512-YLnD62jfgBZYrXIPQcJ0a51Afv9h8VlWqEGK9uU2T5nL/5rb8SnA86+7+mgCZe5D34Tff5RNEA5hjNVJYHzrFg=="],
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
@@ -2085,7 +2086,7 @@
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
"framer-motion": ["framer-motion@8.5.5", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="],
@@ -2683,7 +2684,7 @@
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"miniflare": ["miniflare@4.20251011.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251011.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-5oAaz6lqZus4QFwzEJiNtgpjZR2TBVwBeIhOW33V4gu+l23EukpKja831tFIX2o6sOD/hqZmKZHplOrWl3YGtQ=="],
"miniflare": ["miniflare@4.20251105.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251105.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-n+lCQbGLPjHFm5EKMohxCl+hLIki9rIlJSU9FkYKdJ62cGacetmTH5IgWUZhUFFM+NqhqZLOuWXTAsoZTm0hog=="],
"minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
@@ -2855,7 +2856,7 @@
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="],
"path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
"path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
@@ -3073,7 +3074,7 @@
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="],
"rollup": ["rollup@4.53.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.2", "@rollup/rollup-android-arm64": "4.53.2", "@rollup/rollup-darwin-arm64": "4.53.2", "@rollup/rollup-darwin-x64": "4.53.2", "@rollup/rollup-freebsd-arm64": "4.53.2", "@rollup/rollup-freebsd-x64": "4.53.2", "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", "@rollup/rollup-linux-arm-musleabihf": "4.53.2", "@rollup/rollup-linux-arm64-gnu": "4.53.2", "@rollup/rollup-linux-arm64-musl": "4.53.2", "@rollup/rollup-linux-loong64-gnu": "4.53.2", "@rollup/rollup-linux-ppc64-gnu": "4.53.2", "@rollup/rollup-linux-riscv64-gnu": "4.53.2", "@rollup/rollup-linux-riscv64-musl": "4.53.2", "@rollup/rollup-linux-s390x-gnu": "4.53.2", "@rollup/rollup-linux-x64-gnu": "4.53.2", "@rollup/rollup-linux-x64-musl": "4.53.2", "@rollup/rollup-openharmony-arm64": "4.53.2", "@rollup/rollup-win32-arm64-msvc": "4.53.2", "@rollup/rollup-win32-ia32-msvc": "4.53.2", "@rollup/rollup-win32-x64-gnu": "4.53.2", "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g=="],
"rollup-plugin-visualizer": ["rollup-plugin-visualizer@6.0.5", "", { "dependencies": { "open": "^8.0.0", "picomatch": "^4.0.2", "source-map": "^0.7.4", "yargs": "^17.5.1" }, "peerDependencies": { "rolldown": "1.x || ^1.0.0-beta", "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rolldown", "rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-9+HlNgKCVbJDs8tVtjQ43US12eqaiHyyiLMdBwQ7vSZPiHMysGNo2E88TAp1si5wx8NAoYriI2A5kuKfIakmJg=="],
@@ -3525,9 +3526,9 @@
"wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
"workerd": ["workerd@1.20251011.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251011.0", "@cloudflare/workerd-darwin-arm64": "1.20251011.0", "@cloudflare/workerd-linux-64": "1.20251011.0", "@cloudflare/workerd-linux-arm64": "1.20251011.0", "@cloudflare/workerd-windows-64": "1.20251011.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Dq35TLPEJAw7BuYQMkN3p9rge34zWMU2Gnd4DSJFeVqld4+DAO2aPG7+We2dNIAyM97S8Y9BmHulbQ00E0HC7Q=="],
"workerd": ["workerd@1.20251105.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251105.0", "@cloudflare/workerd-darwin-arm64": "1.20251105.0", "@cloudflare/workerd-linux-64": "1.20251105.0", "@cloudflare/workerd-linux-arm64": "1.20251105.0", "@cloudflare/workerd-windows-64": "1.20251105.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-8D1UmsxrRr3Go7enbYCsYoiWeGn66u1WFNojPSgtjp7z8pV2cXskjr05vQ1OOzl7+rg1hDDofnCJqVwChMym8g=="],
"wrangler": ["wrangler@4.45.4", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.9", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251011.2", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251011.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251011.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-niXT7B463wQi7WXIHjYK8txgWhuKQLrGmhjoR58SnPhlkq4wGjd3rFrkVyRc/O58clGTfs672BSGOph4XMoQKw=="],
"wrangler": ["wrangler@4.46.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.9", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251105.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251105.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251014.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-WRROO7CL+MW/E44RMT4X7w32qPjufiPpGdey5D6H7iKzzVqfUkTRULxYBfWANiU1yGnsiCXQtu3Ap0G2TmohtA=="],
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
@@ -3557,7 +3558,7 @@
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="],
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
"yocto-spinner": ["yocto-spinner@0.2.3", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ=="],
@@ -3727,6 +3728,8 @@
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
"@opencode-ai/desktop/@solidjs/router": ["@solidjs/router@0.15.3", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw=="],
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="],
"@opencode-ai/web/@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="],
@@ -3799,8 +3802,6 @@
"@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
"@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
"@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@vercel/nft/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
@@ -3909,6 +3910,8 @@
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"gel/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"giget/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
@@ -3961,7 +3964,7 @@
"named-placeholders/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"nitropack/c12": ["c12@3.3.1", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-LcWQ01LT9tkoUINHgpIOv3mMs+Abv7oVCrtpMRi1PaapVEpWoMga5WuT7/DqFTu7URP9ftbOmimNw1KNIGh9DQ=="],
"nitropack/c12": ["c12@3.3.2", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A=="],
"nitropack/globby": ["globby@15.0.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "fast-glob": "^3.3.3", "ignore": "^7.0.5", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" } }, "sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw=="],
+1 -1
View File
@@ -1,2 +1,2 @@
[install]
exact = true
exact = true
+1 -1
View File
@@ -4,7 +4,7 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "bun@1.3.1",
"packageManager": "bun@1.3.2",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"typecheck": "bun turbo typecheck",
+1 -1
View File
@@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "1.0.55"
"version": "1.0.62"
},
"dependencies": {
"@ibm/plex": "6.4.1",
@@ -19,7 +19,7 @@ const getUserEmail = query(async (workspaceID: string) => {
export default function WorkspaceLayout(props: RouteSectionProps) {
const params = useParams()
const userEmail = createAsync(() => getUserEmail(params.id))
const userEmail = createAsync(() => getUserEmail(params.id!))
return (
<main data-page="workspace">
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
@@ -5,7 +5,7 @@ import "./[id].css"
export default function WorkspaceLayout(props: RouteSectionProps) {
const params = useParams()
const userInfo = createAsync(() => querySessionInfo(params.id))
const userInfo = createAsync(() => querySessionInfo(params.id!))
return (
<main data-page="workspace">
@@ -27,7 +27,7 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
export function BillingSection() {
const params = useParams()
// ORIGINAL CODE - COMMENTED OUT FOR TESTING
const billingInfo = createAsync(() => queryBillingInfo(params.id))
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
const checkoutAction = useAction(createCheckoutUrl)
const checkoutSubmission = useSubmission(createCheckoutUrl)
const sessionAction = useAction(createSessionUrl)
@@ -51,7 +51,7 @@ export function BillingSection() {
const amount = parseInt(store.addBalanceAmount)
const baseUrl = window.location.href
const checkout = await checkoutAction(params.id, amount, baseUrl, baseUrl)
const checkout = await checkoutAction(params.id!, amount, baseUrl, baseUrl)
if (checkout && checkout.data) {
setStore("checkoutRedirecting", true)
window.location.href = checkout.data
@@ -60,7 +60,7 @@ export function BillingSection() {
async function onClickSession() {
const baseUrl = window.location.href
const sessionUrl = await sessionAction(params.id, baseUrl)
const sessionUrl = await sessionAction(params.id!, baseUrl)
if (sessionUrl && sessionUrl.data) {
setStore("sessionRedirecting", true)
window.location.href = sessionUrl.data
@@ -8,8 +8,8 @@ import { queryBillingInfo, querySessionInfo } from "../../common"
export default function () {
const params = useParams()
const userInfo = createAsync(() => querySessionInfo(params.id))
const billingInfo = createAsync(() => queryBillingInfo(params.id))
const userInfo = createAsync(() => querySessionInfo(params.id!))
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
return (
<div data-page="workspace-[id]">
@@ -30,7 +30,7 @@ export function MonthlyLimitSection() {
const params = useParams()
const submission = useSubmission(setMonthlyLimit)
const [store, setStore] = createStore({ show: false })
const billingInfo = createAsync(() => queryBillingInfo(params.id))
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
let input: HTMLInputElement
@@ -19,7 +19,7 @@ const downloadReceipt = action(async (workspaceID: string, paymentID: string) =>
export function PaymentSection() {
const params = useParams()
const payments = createAsync(() => getPaymentsInfo(params.id))
const payments = createAsync(() => getPaymentsInfo(params.id!))
const downloadReceiptAction = useAction(downloadReceipt)
// DUMMY DATA FOR TESTING
@@ -89,7 +89,7 @@ export function PaymentSection() {
<td data-slot="payment-receipt">
<button
onClick={async () => {
const receiptUrl = await downloadReceiptAction(params.id, payment.paymentID!)
const receiptUrl = await downloadReceiptAction(params.id!, payment.paymentID!)
if (receiptUrl) {
window.open(receiptUrl, "_blank")
}
@@ -58,7 +58,7 @@ const setReload = action(async (form: FormData) => {
export function ReloadSection() {
const params = useParams()
const billingInfo = createAsync(() => queryBillingInfo(params.id))
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
const setReloadSubmission = useSubmission(setReload)
const reloadSubmission = useSubmission(reload)
const [store, setStore] = createStore({
@@ -10,8 +10,8 @@ import { querySessionInfo, queryBillingInfo, createCheckoutUrl, formatBalance }
export default function () {
const params = useParams()
const userInfo = createAsync(() => querySessionInfo(params.id))
const billingInfo = createAsync(() => queryBillingInfo(params.id))
const userInfo = createAsync(() => querySessionInfo(params.id!))
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
const checkoutAction = useAction(createCheckoutUrl)
const checkoutSubmission = useSubmission(createCheckoutUrl)
const [store, setStore] = createStore({
@@ -21,7 +21,7 @@ export default function () {
async function onClickCheckout() {
const baseUrl = window.location.href
const checkout = await checkoutAction(params.id, billingInfo()!.reloadAmount, baseUrl, baseUrl)
const checkout = await checkoutAction(params.id!, billingInfo()!.reloadAmount, baseUrl, baseUrl)
if (checkout && checkout.data) {
setStore("checkoutRedirecting", true)
window.location.href = checkout.data
@@ -45,7 +45,7 @@ const listKeys = query(async (workspaceID: string) => {
export function KeySection() {
const params = useParams()
const keys = createAsync(() => listKeys(params.id))
const keys = createAsync(() => listKeys(params.id!))
const submission = useSubmission(createKey)
const [store, setStore] = createStore({ show: false })
@@ -209,7 +209,7 @@ const roleOptions = [
export function MemberSection() {
const params = useParams()
const data = createAsync(() => listMembers(params.id))
const data = createAsync(() => listMembers(params.id!))
const submission = useSubmission(inviteMember)
const [store, setStore] = createStore({
show: false,
@@ -328,7 +328,7 @@ export function MemberSection() {
{(member) => (
<MemberRow
member={member}
workspaceID={params.id}
workspaceID={params.id!}
actorID={data()!.actorID}
actorRole={data()!.actorRole}
/>
@@ -52,8 +52,8 @@ const updateModel = action(async (form: FormData) => {
export function ModelSection() {
const params = useParams()
const modelsInfo = createAsync(() => getModelsInfo(params.id))
const userInfo = createAsync(() => querySessionInfo(params.id))
const modelsInfo = createAsync(() => getModelsInfo(params.id!))
const userInfo = createAsync(() => querySessionInfo(params.id!))
const modelsWithLab = createMemo(() => {
const info = modelsInfo()
@@ -21,8 +21,8 @@ const listKeys = query(async (workspaceID: string) => {
export function NewUserSection() {
const params = useParams()
const [copiedKey, setCopiedKey] = createSignal(false)
const keys = createAsync(() => listKeys(params.id))
const usage = createAsync(() => getUsageInfo(params.id))
const keys = createAsync(() => listKeys(params.id!))
const usage = createAsync(() => getUsageInfo(params.id!))
const isNew = createMemo(() => {
const keysList = keys()
const usageList = usage()
@@ -54,7 +54,7 @@ const listProviders = query(async (workspaceID: string) => {
function ProviderRow(props: { provider: Provider }) {
const params = useParams()
const providers = createAsync(() => listProviders(params.id))
const providers = createAsync(() => listProviders(params.id!))
const saveSubmission = useSubmission(saveProvider, ([fd]) => fd.get("provider")?.toString() === props.provider.key)
const removeSubmission = useSubmission(
removeProvider,
@@ -46,7 +46,7 @@ const updateWorkspace = action(async (form: FormData) => {
export function SettingsSection() {
const params = useParams()
const workspaceInfo = createAsync(() => getWorkspaceInfo(params.id))
const workspaceInfo = createAsync(() => getWorkspaceInfo(params.id!))
const submission = useSubmission(updateWorkspace)
const [store, setStore] = createStore({ show: false })
@@ -15,7 +15,7 @@ const getUsageInfo = query(async (workspaceID: string) => {
export function UsageSection() {
const params = useParams()
// ORIGINAL CODE - COMMENTED OUT FOR TESTING
const usage = createAsync(() => getUsageInfo(params.id))
const usage = createAsync(() => getUsageInfo(params.id!))
// DUMMY DATA FOR TESTING
// const usage = () => [
@@ -20,6 +20,10 @@ import { oaCompatHelper } from "./provider/openai-compatible"
import { createRateLimiter } from "./rateLimiter"
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type RetryOptions = {
excludeProviders: string[]
retryCount: number
}
export async function handler(
input: APIEvent,
@@ -32,6 +36,7 @@ export async function handler(
type ModelInfo = Awaited<ReturnType<typeof validateModel>>
type ProviderInfo = Awaited<ReturnType<typeof selectProvider>>
const MAX_RETRIES = 3
const FREE_WORKSPACES = [
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
"wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
@@ -47,40 +52,56 @@ export async function handler(
})
const zenData = ZenData.list()
const modelInfo = validateModel(zenData, body.model)
const providerInfo = selectProvider(zenData, modelInfo, ip)
const authInfo = await authenticate(modelInfo, providerInfo)
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
await rateLimiter?.check()
validateBilling(authInfo, modelInfo)
validateModelSettings(authInfo)
updateProviderKey(authInfo, providerInfo)
logger.metric({ provider: providerInfo.id })
// Request to model provider
const startTimestamp = Date.now()
const reqUrl = providerInfo.modifyUrl(providerInfo.api)
const reqBody = JSON.stringify(
providerInfo.modifyBody({
...createBodyConverter(opts.format, providerInfo.format)(body),
model: providerInfo.model,
}),
)
logger.debug("REQUEST URL: " + reqUrl)
logger.debug("REQUEST: " + reqBody.substring(0, 300) + "...")
const res = await fetch(reqUrl, {
method: "POST",
headers: (() => {
const headers = input.request.headers
headers.delete("host")
headers.delete("content-length")
providerInfo.modifyHeaders(headers, body, providerInfo.apiKey)
Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => {
headers.set(k, headers.get(v)!)
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
const providerInfo = selectProvider(zenData, modelInfo, ip, retry)
const authInfo = await authenticate(modelInfo, providerInfo)
validateBilling(authInfo, modelInfo)
validateModelSettings(authInfo)
updateProviderKey(authInfo, providerInfo)
logger.metric({ provider: providerInfo.id })
const startTimestamp = Date.now()
const reqUrl = providerInfo.modifyUrl(providerInfo.api)
const reqBody = JSON.stringify(
providerInfo.modifyBody({
...createBodyConverter(opts.format, providerInfo.format)(body),
model: providerInfo.model,
}),
)
logger.debug("REQUEST URL: " + reqUrl)
logger.debug("REQUEST: " + reqBody.substring(0, 300) + "...")
const res = await fetch(reqUrl, {
method: "POST",
headers: (() => {
const headers = new Headers(input.request.headers)
providerInfo.modifyHeaders(headers, body, providerInfo.apiKey)
Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => {
headers.set(k, headers.get(v)!)
})
headers.delete("host")
headers.delete("content-length")
headers.delete("x-opencode-request")
headers.delete("x-opencode-session")
return headers
})(),
body: reqBody,
})
// Try another provider => stop retrying if using fallback provider
if (res.status !== 200 && modelInfo.fallbackProvider && providerInfo.id !== modelInfo.fallbackProvider) {
return retriableRequest({
excludeProviders: [...retry.excludeProviders, providerInfo.id],
retryCount: retry.retryCount + 1,
})
return headers
})(),
body: reqBody,
})
}
return { providerInfo, authInfo, res, startTimestamp }
}
const { providerInfo, authInfo, res, startTimestamp } = await retriableRequest()
// Scrub response headers
const resHeaders = new Headers()
@@ -236,19 +257,25 @@ export async function handler(
return { id: modelId, ...modelData }
}
function selectProvider(zenData: ZenData, modelInfo: ModelInfo, ip: string) {
const providers = modelInfo.providers
.filter((provider) => !provider.disabled)
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
function selectProvider(zenData: ZenData, modelInfo: ModelInfo, ip: string, retry: RetryOptions) {
const provider = (() => {
if (retry.retryCount === MAX_RETRIES) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
}
// Use the last 2 characters of IP address to select a provider
const lastChars = ip.slice(-2)
const index = parseInt(lastChars, 16) % providers.length
const provider = providers[index || 0]
const providers = modelInfo.providers
.filter((provider) => !provider.disabled)
.filter((provider) => !retry.excludeProviders.includes(provider.id))
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
if (!(provider.id in zenData.providers)) {
throw new ModelError(`Provider ${provider.id} not supported`)
}
// Use the last 2 characters of IP address to select a provider
const lastChars = ip.slice(-2)
const index = parseInt(lastChars, 16) % providers.length
return providers[index || 0]
})()
if (!provider) throw new ModelError("No provider available")
if (!(provider.id in zenData.providers)) throw new ModelError(`Provider ${provider.id} not supported`)
return {
...provider,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.55",
"version": "1.0.62",
"private": true,
"type": "module",
"dependencies": {
+1
View File
@@ -25,6 +25,7 @@ export namespace ZenData {
cost200K: ModelCostSchema.optional(),
allowAnonymous: z.boolean().optional(),
rateLimit: z.number().optional(),
fallbackProvider: z.string().optional(),
providers: z.array(
z.object({
id: z.string(),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.55",
"version": "1.0.62",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.55",
"version": "1.0.62",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.55",
"version": "1.0.62",
"description": "",
"type": "module",
"scripts": {
+63 -114
View File
@@ -64,7 +64,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const handleFileSelect = (path: string | undefined) => {
if (!path) return
addPart({ type: "file", path, content: "@" + getFilename(path), start: 0, end: 0 })
setStore("popoverIsOpen", false)
}
const { flat, active, onInput, onKeyDown, refetch } = useFilteredList<string>({
@@ -114,16 +113,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
),
)
createEffect(
on(
() => session.prompt.cursor(),
(cursor) => {
if (cursor === undefined) return
queueMicrotask(() => setCursorPosition(editorRef, cursor))
},
),
)
const parseFromDOM = (): Prompt => {
const newParts: Prompt = []
let position = 0
@@ -173,118 +162,70 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
const addPart = (part: ContentPart) => {
const selection = window.getSelection()
if (!selection || selection.rangeCount === 0) return
const cursorPosition = getCursorPosition(editorRef)
const prompt = session.prompt.current()
const rawText = prompt.map((p) => p.content).join("")
const textBeforeCursor = rawText.substring(0, cursorPosition)
const atMatch = textBeforeCursor.match(/@(\S*)$/)
const startIndex = atMatch ? atMatch.index! : cursorPosition
const endIndex = atMatch ? cursorPosition : cursorPosition
if (part.type === "file") {
const pill = document.createElement("span")
pill.textContent = part.content
pill.setAttribute("data-type", "file")
pill.setAttribute("data-path", part.path)
pill.setAttribute("contenteditable", "false")
pill.style.userSelect = "text"
pill.style.cursor = "default"
const pushText = (acc: { parts: ContentPart[]; runningIndex: number }, value: string) => {
if (!value) return
const last = acc.parts[acc.parts.length - 1]
if (last && last.type === "text") {
acc.parts[acc.parts.length - 1] = {
type: "text",
content: last.content + value,
start: last.start,
end: last.end + value.length,
const gap = document.createTextNode(" ")
const range = selection.getRangeAt(0)
if (atMatch) {
let node: Node | null = range.startContainer
let offset = range.startOffset
let runningLength = 0
const walker = document.createTreeWalker(editorRef, NodeFilter.SHOW_TEXT, null)
let currentNode = walker.nextNode()
while (currentNode) {
const textContent = currentNode.textContent || ""
if (runningLength + textContent.length >= atMatch.index!) {
const localStart = atMatch.index! - runningLength
const localEnd = cursorPosition - runningLength
if (currentNode === range.startContainer || runningLength + textContent.length >= cursorPosition) {
range.setStart(currentNode, localStart)
range.setEnd(currentNode, Math.min(localEnd, textContent.length))
break
}
}
runningLength += textContent.length
currentNode = walker.nextNode()
}
return
}
acc.parts.push({ type: "text", content: value, start: acc.runningIndex, end: acc.runningIndex + value.length })
range.deleteContents()
range.insertNode(gap)
range.insertNode(pill)
range.setStartAfter(gap)
range.collapse(true)
selection.removeAllRanges()
selection.addRange(range)
} else if (part.type === "text") {
const textNode = document.createTextNode(part.content)
const range = selection.getRangeAt(0)
range.deleteContents()
range.insertNode(textNode)
range.setStartAfter(textNode)
range.collapse(true)
selection.removeAllRanges()
selection.addRange(range)
}
const {
parts: nextParts,
inserted,
cursorPositionAfter,
} = prompt.reduce(
(acc, item) => {
if (acc.inserted) {
acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length })
acc.runningIndex += item.content.length
return acc
}
const nextIndex = acc.runningIndex + item.content.length
if (nextIndex <= startIndex) {
acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length })
acc.runningIndex = nextIndex
return acc
}
if (item.type !== "text") {
acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length })
acc.runningIndex = nextIndex
return acc
}
const headLength = Math.max(0, startIndex - acc.runningIndex)
const tailLength = Math.max(0, endIndex - acc.runningIndex)
const head = item.content.slice(0, headLength)
const tail = item.content.slice(tailLength)
pushText(acc, head)
acc.runningIndex += head.length
if (part.type === "text") {
pushText(acc, part.content)
acc.runningIndex += part.content.length
}
if (part.type !== "text") {
acc.parts.push({ ...part, start: acc.runningIndex, end: acc.runningIndex + part.content.length })
acc.runningIndex += part.content.length
}
const needsGap = Boolean(atMatch)
const rest = needsGap ? (tail ? (/^\s/.test(tail) ? tail : ` ${tail}`) : " ") : tail
pushText(acc, rest)
acc.runningIndex += rest.length
const baseCursor = startIndex + part.content.length
const cursorAddition = needsGap && rest.length > 0 ? 1 : 0
acc.cursorPositionAfter = baseCursor + cursorAddition
acc.inserted = true
return acc
},
{
parts: [] as ContentPart[],
runningIndex: 0,
inserted: false,
cursorPositionAfter: cursorPosition + part.content.length,
},
)
if (!inserted) {
const baseParts = prompt.filter((item) => !(item.type === "text" && item.content === ""))
const runningIndex = baseParts.reduce((sum, p) => sum + p.content.length, 0)
const appendedAcc = { parts: [...baseParts] as ContentPart[], runningIndex }
if (part.type === "text") {
pushText(appendedAcc, part.content)
}
if (part.type !== "text") {
appendedAcc.parts.push({
...part,
start: appendedAcc.runningIndex,
end: appendedAcc.runningIndex + part.content.length,
})
}
const next = appendedAcc.parts.length > 0 ? appendedAcc.parts : DEFAULT_PROMPT
const nextCursor = rawText.length + part.content.length
session.prompt.set(next, nextCursor)
setStore("popoverIsOpen", false)
queueMicrotask(() => setCursorPosition(editorRef, nextCursor))
return
}
session.prompt.set(nextParts, cursorPositionAfter)
handleInput()
setStore("popoverIsOpen", false)
queueMicrotask(() => setCursorPosition(editorRef, cursorPositionAfter))
}
const abort = () =>
@@ -378,7 +319,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
session.layout.setActiveTab(undefined)
session.messages.setActive(undefined)
session.prompt.set(DEFAULT_PROMPT, 0)
// Clear the editor DOM directly to ensure it's empty
editorRef.innerHTML = ""
session.prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
sdk.client.session.prompt({
path: { id: existing.id },
@@ -402,7 +345,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return (
<div class="relative size-full _max-h-[320px] flex flex-col gap-3">
<Show when={store.popoverIsOpen}>
<div class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-[252px] min-h-10 overflow-y-auto flex flex-col p-2 pb-0 rounded-2xl border border-border-base bg-surface-raised-stronger-non-alpha shadow-md">
<div
class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-[252px] min-h-10
overflow-auto no-scrollbar flex flex-col p-2 pb-0 rounded-2xl
border border-border-base bg-surface-raised-stronger-non-alpha shadow-md"
>
<Show when={flat().length > 0} fallback={<div class="text-text-weak px-2">No matching files</div>}>
<For each={flat()}>
{(i) => (
@@ -419,7 +366,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(i)}
</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
<Show when={!i.endsWith("/")}>
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
</Show>
</div>
</div>
<div class="flex items-center gap-x-1 text-text-muted/40 shrink-0"></div>
+25 -7
View File
@@ -15,18 +15,19 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
const [store, setStore] = makePersisted(
createStore<{
prompt: Prompt
cursorPosition?: number
messageId?: string
tabs: {
active?: string
opened: string[]
}
prompt: Prompt
cursor?: number
}>({
prompt: [{ type: "text", content: "", start: 0, end: 0 }],
tabs: {
opened: [],
},
prompt: clonePrompt(DEFAULT_PROMPT),
cursor: undefined,
}),
{
name: props.sessionId ?? "new-session",
@@ -102,12 +103,13 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
diffs,
prompt: {
current: createMemo(() => store.prompt),
cursor: createMemo(() => store.cursorPosition),
cursor: createMemo(() => store.cursor),
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
set(prompt: Prompt, cursorPosition?: number) {
const next = clonePrompt(prompt)
batch(() => {
setStore("prompt", prompt)
if (cursorPosition !== undefined) setStore("cursorPosition", cursorPosition)
setStore("prompt", next)
if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
})
},
},
@@ -172,7 +174,6 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
opened.splice(to, 0, opened.splice(index, 1)[0])
}),
)
// setStore("node", path, "pinned", true)
},
},
}
@@ -215,3 +216,20 @@ export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
}
return true
}
function cloneSelection(selection?: TextSelection) {
if (!selection) return undefined
return { ...selection }
}
function clonePart(part: ContentPart): ContentPart {
if (part.type === "text") return { ...part }
return {
...part,
selection: cloneSelection(part.selection),
}
}
function clonePrompt(prompt: Prompt): Prompt {
return prompt.map(clonePart)
}
+1 -1
View File
@@ -216,7 +216,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
setStore("limit", (x) => x + count)
await load.session()
},
more: createMemo(() => store.session.length === store.limit),
more: createMemo(() => store.session.length >= store.limit),
},
load,
absolute,
+36 -23
View File
@@ -44,7 +44,7 @@ import {
useDragDropContext,
} from "@thisbeyond/solid-dnd"
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import type { JSX } from "solid-js"
import type { JSX, ParentProps } from "solid-js"
import { useSync } from "@/context/sync"
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
import { Markdown } from "@opencode-ai/ui"
@@ -332,11 +332,12 @@ export default function Page() {
<Tabs.Content value="chat" class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
<div
classList={{
"w-full grid flex-1 min-h-0": true,
"grid-cols-2": local.layout.review.state() === "open",
"w-full flex-1 min-h-0": true,
grid: local.layout.review.state() !== "open",
flex: local.layout.review.state() === "open",
}}
>
<div class="relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0 max-w-2xl mx-auto">
<div class="relative shrink-0 px-6 py-2 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-xl mx-auto">
<Switch>
<Match when={session.id}>
<div class="h-8 flex shrink-0 self-stretch items-center justify-end">
@@ -357,8 +358,8 @@ export default function Page() {
role="list"
classList={{
"mr-8 shrink-0 flex flex-col items-start": true,
"absolute right-full w-60 @7xl:gap-2": true, // local.layout.review.state() !== "open",
"": local.layout.review.state() === "open",
"absolute right-full w-60 @7xl:gap-2": local.layout.review.state() !== "open",
"mt-1": local.layout.review.state() === "open",
}}
>
<For each={session.messages.user()}>
@@ -436,7 +437,7 @@ export default function Page() {
</For>
</ul>
</Show>
<div ref={messageScrollElement} class="grow w-full min-w-0 h-full overflow-y-auto no-scrollbar">
<div ref={messageScrollElement} class="grow size-full min-w-0 overflow-y-auto no-scrollbar">
<For each={session.messages.user()}>
{(message) => {
const isActive = createMemo(() => session.messages.active()?.id === message.id)
@@ -476,7 +477,7 @@ export default function Page() {
class="flex flex-col items-start self-stretch gap-8 pb-20"
>
{/* Title */}
<div class="flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10 pb-1">
<div class="flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-20 pb-1">
<div class="w-full text-14-medium text-text-strong">
<Show
when={titled()}
@@ -523,7 +524,7 @@ export default function Page() {
<For each={message.summary?.diffs ?? []}>
{(diff) => (
<Accordion.Item value={diff.file}>
<Accordion.Header>
<StickyAccordionHeader class="top-10 data-expanded:before:-top-10 ">
<Accordion.Trigger>
<div class="flex items-center justify-between w-full gap-5">
<div class="grow flex items-center gap-5 min-w-0">
@@ -548,8 +549,8 @@ export default function Page() {
</div>
</div>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="max-h-[360px] overflow-y-auto no-scrollbar">
</StickyAccordionHeader>
<Accordion.Content class="max-h-60 overflow-y-auto no-scrollbar">
<Diff
before={{
name: diff.file!,
@@ -655,7 +656,7 @@ export default function Page() {
<Show when={local.layout.review.state() === "open"}>
<div
classList={{
"relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0 border-l border-border-weak-base": true,
"relative grow px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0 border-l border-border-weak-base": true,
}}
>
<div class="h-8 w-full flex items-center justify-between shrink-0 self-stretch">
@@ -681,7 +682,7 @@ export default function Page() {
<For each={session.diffs()}>
{(diff) => (
<Accordion.Item value={diff.file} defaultOpen>
<Accordion.Header>
<StickyAccordionHeader>
<Accordion.Trigger>
<div class="flex items-center justify-between w-full gap-5">
<div class="grow flex items-center gap-5 min-w-0">
@@ -701,7 +702,7 @@ export default function Page() {
</div>
</div>
</Accordion.Trigger>
</Accordion.Header>
</StickyAccordionHeader>
<Accordion.Content>
<Diff
before={{
@@ -724,22 +725,19 @@ export default function Page() {
</div>
</Tabs.Content>
<Show when={local.layout.review.state() === "tab" && session.diffs().length}>
<Tabs.Content value="review" class="select-text">
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden mt-8">
<div
classList={{
"relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0": true,
"relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0 overflow-hidden": true,
}}
>
<div class="h-8 w-full flex items-center justify-between shrink-0 self-stretch sticky top-0 bg-background-stronger z-100">
<div class="flex items-center gap-x-3"></div>
</div>
<div class="text-14-medium text-text-strong">All changes</div>
<div class="h-full pb-40 overflow-y-auto no-scrollbar">
<div class="text-14-medium text-text-strong shrink-0">All changes</div>
<div class="flex-1 min-h-0 pb-40 overflow-y-auto no-scrollbar">
<Accordion class="w-full" multiple>
<For each={session.diffs()}>
{(diff) => (
<Accordion.Item value={diff.file} defaultOpen>
<Accordion.Header>
<StickyAccordionHeader>
<Accordion.Trigger>
<div class="flex items-center justify-between w-full gap-5">
<div class="grow flex items-center gap-5 min-w-0">
@@ -757,7 +755,7 @@ export default function Page() {
</div>
</div>
</Accordion.Trigger>
</Accordion.Header>
</StickyAccordionHeader>
<Accordion.Content>
<Diff
diffStyle="split"
@@ -897,3 +895,18 @@ export default function Page() {
</div>
)
}
function StickyAccordionHeader(props: ParentProps<{ class?: string }>) {
return (
<Accordion.Header
classList={{
"sticky top-0 data-expanded:z-10": true,
"data-expanded:before:content-[''] data-expanded:before:z-[-10]": true,
"data-expanded:before:absolute data-expanded:before:inset-0 data-expanded:before:bg-background-stronger": true,
[props.class ?? ""]: !!props.class,
}}
>
{props.children}
</Accordion.Header>
)
}
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The AI coding agent built for the terminal"
version = "1.0.55"
version = "1.0.62"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.55/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.62/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.55/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.62/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.55/opencode-linux-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.62/opencode-linux-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.55/opencode-linux-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.62/opencode-linux-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.55/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.62/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.55",
"version": "1.0.62",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.55",
"version": "1.0.62",
"name": "opencode",
"type": "module",
"private": true,
@@ -54,8 +54,8 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.0.0-20251108-0c7899b1",
"@opentui/solid": "0.0.0-20251108-0c7899b1",
"@opentui/core": "0.1.42",
"@opentui/solid": "0.1.42",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
+74 -25
View File
@@ -17,38 +17,87 @@ import { Script } from "@opencode-ai/script"
const singleFlag = process.argv.includes("--single")
const allTargets = [
["windows", "x64"],
["linux", "arm64"],
["linux", "x64"],
["linux", "x64-baseline"],
["darwin", "x64"],
["darwin", "x64-baseline"],
["darwin", "arm64"],
const allTargets: {
os: string
arch: "arm64" | "x64"
abi?: "musl"
avx2?: false
}[] = [
{
os: "linux",
arch: "arm64",
},
{
os: "linux",
arch: "x64",
},
{
os: "linux",
arch: "x64",
avx2: false,
},
{
os: "linux",
arch: "arm64",
abi: "musl",
},
{
os: "linux",
arch: "x64",
abi: "musl",
},
{
os: "linux",
arch: "x64",
abi: "musl",
avx2: false,
},
{
os: "darwin",
arch: "arm64",
},
{
os: "darwin",
arch: "x64",
},
{
os: "darwin",
arch: "x64",
avx2: false,
},
{
os: "windows",
arch: "x64",
},
{
os: "windows",
arch: "x64",
avx2: false,
},
]
const targets = singleFlag
? allTargets.filter(([os, arch]) => os === process.platform && arch === process.arch)
? allTargets.filter((item) => item.os === process.platform && item.arch === process.arch)
: allTargets
await $`rm -rf dist`
const binaries: Record<string, string> = {}
for (const [os, arch] of targets) {
console.log(`building ${os}-${arch}`)
const name = `${pkg.name}-${os}-${arch}`
await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}`
for (const item of targets) {
const name = [
pkg.name,
item.os,
item.arch,
item.avx2 === false ? "baseline" : undefined,
item.abi === undefined ? undefined : item.abi,
]
.filter(Boolean)
.join("-")
console.log(`building ${name}`)
await $`mkdir -p dist/${name}/bin`
const opentui = `@opentui/core-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}`
await $`mkdir -p ../../node_modules/${opentui}`
await $`npm pack ${opentui}@${pkg.dependencies["@opentui/core"]}`.cwd(path.join(dir, "../../node_modules"))
await $`tar -xf ../../node_modules/${opentui.replace("@opentui/", "opentui-")}-*.tgz -C ../../node_modules/${opentui} --strip-components=1`
const watcher = `@parcel/watcher-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}${os === "linux" ? "-glibc" : ""}`
await $`mkdir -p ../../node_modules/${watcher}`
await $`npm pack ${watcher}`.cwd(path.join(dir, "../../node_modules")).quiet()
await $`tar -xf ../../node_modules/${watcher.replace("@parcel/", "parcel-")}-*.tgz -C ../../node_modules/${watcher} --strip-components=1`
const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"))
const workerPath = "./src/cli/cmd/tui/worker.ts"
@@ -58,7 +107,7 @@ for (const [os, arch] of targets) {
plugins: [solidPlugin],
sourcemap: "external",
compile: {
target: `bun-${os}-${arch}` as any,
target: name.replace(pkg.name, "bun") as any,
outfile: `dist/${name}/bin/opencode`,
execArgv: [`--user-agent=opencode/${Script.version}`, `--env-file=""`, `--`],
windows: {},
@@ -78,8 +127,8 @@ for (const [os, arch] of targets) {
{
name,
version: Script.version,
os: [os === "windows" ? "win32" : os],
cpu: [arch],
os: [item.os === "windows" ? "win32" : item.os],
cpu: [item.arch],
},
null,
2,
+3 -1
View File
@@ -16,6 +16,7 @@ export namespace Agent {
builtIn: z.boolean(),
topP: z.number().optional(),
temperature: z.number().optional(),
color: z.string().optional(),
permission: z.object({
edit: Config.Permission,
bash: z.record(z.string(), Config.Permission),
@@ -147,7 +148,7 @@ export namespace Agent {
tools: {},
builtIn: false,
}
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, color, ...extra } = value
item.options = {
...item.options,
...extra,
@@ -167,6 +168,7 @@ export namespace Agent {
if (temperature != undefined) item.temperature = temperature
if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode
if (color) item.color = color
// just here for consistency & to prevent it from being added as an option
if (name) item.name = name
+11 -5
View File
@@ -439,9 +439,10 @@ export const GithubRunCommand = cmd({
// Local PR
if (prData.headRepository.nameWithOwner === prData.baseRepository.nameWithOwner) {
await checkoutLocalBranch(prData)
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const dataPrompt = buildPromptDataForPR(prData)
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
if (await branchIsDirty()) {
if (await branchIsDirty(head)) {
const summary = await summarize(response)
await pushToLocalBranch(summary)
}
@@ -451,9 +452,10 @@ export const GithubRunCommand = cmd({
// Fork PR
else {
await checkoutForkBranch(prData)
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const dataPrompt = buildPromptDataForPR(prData)
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
if (await branchIsDirty()) {
if (await branchIsDirty(head)) {
const summary = await summarize(response)
await pushToForkBranch(summary, prData)
}
@@ -464,10 +466,11 @@ export const GithubRunCommand = cmd({
// Issue
else {
const branch = await checkoutNewBranch()
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const issueData = await fetchIssue()
const dataPrompt = buildPromptDataForIssue(issueData)
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
if (await branchIsDirty()) {
if (await branchIsDirty(head)) {
const summary = await summarize(response)
await pushToNewBranch(summary, branch)
const pr = await createPR(
@@ -832,10 +835,13 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
await $`git push fork HEAD:${remoteBranch}`
}
async function branchIsDirty() {
async function branchIsDirty(originalHead: string) {
console.log("Checking if branch is dirty...")
const ret = await $`git status --porcelain`
return ret.stdout.toString().trim().length > 0
const status = ret.stdout.toString().trim()
if (status.length > 0) return true
const head = await $`git rev-parse HEAD`
return head.stdout.toString().trim() !== originalHead
}
async function assertPermissions() {
+41 -5
View File
@@ -1,21 +1,57 @@
import type { Argv } from "yargs"
import { Instance } from "../../project/instance"
import { Provider } from "../../provider/provider"
import { cmd } from "./cmd"
import { UI } from "../ui"
import { EOL } from "os"
export const ModelsCommand = cmd({
command: "models",
command: "models [provider]",
describe: "list all available models",
handler: async () => {
builder: (yargs: Argv) => {
return yargs
.positional("provider", {
describe: "provider ID to filter models by",
type: "string",
array: false,
})
.option("verbose", {
describe: "use more verbose model output (includes metadata like costs)",
type: "boolean",
})
},
handler: async (args) => {
await Instance.provide({
directory: process.cwd(),
async fn() {
const providers = await Provider.list()
for (const [providerID, provider] of Object.entries(providers)) {
for (const modelID of Object.keys(provider.info.models)) {
console.log(`${providerID}/${modelID}`)
function printModels(providerID: string, verbose?: boolean) {
const provider = providers[providerID]
for (const [modelID, model] of Object.entries(provider.info.models)) {
process.stdout.write(`${providerID}/${modelID}`)
process.stdout.write(EOL)
if (verbose) {
process.stdout.write(JSON.stringify(model, null, 2))
process.stdout.write(EOL)
}
}
}
if (args.provider) {
const provider = providers[args.provider]
if (!provider) {
UI.error(`Provider not found: ${args.provider}`)
return
}
printModels(args.provider, args.verbose)
return
}
for (const providerID of Object.keys(providers)) {
printModels(providerID, args.verbose)
}
},
})
},
+112
View File
@@ -0,0 +1,112 @@
import { UI } from "../ui"
import { cmd } from "./cmd"
import { Instance } from "@/project/instance"
import { $ } from "bun"
export const PrCommand = cmd({
command: "pr <number>",
describe: "fetch and checkout a GitHub PR branch, then run opencode",
builder: (yargs) =>
yargs.positional("number", {
type: "number",
describe: "PR number to checkout",
demandOption: true,
}),
async handler(args) {
await Instance.provide({
directory: process.cwd(),
async fn() {
const project = Instance.project
if (project.vcs !== "git") {
UI.error("Could not find git repository. Please run this command from a git repository.")
process.exit(1)
}
const prNumber = args.number
const localBranchName = `pr/${prNumber}`
UI.println(`Fetching and checking out PR #${prNumber}...`)
// Use gh pr checkout with custom branch name
const result = await $`gh pr checkout ${prNumber} --branch ${localBranchName} --force`.nothrow()
if (result.exitCode !== 0) {
UI.error(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`)
process.exit(1)
}
// Fetch PR info for fork handling and session link detection
const prInfoResult =
await $`gh pr view ${prNumber} --json headRepository,headRepositoryOwner,isCrossRepository,headRefName,body`.nothrow()
let sessionId: string | undefined
if (prInfoResult.exitCode === 0) {
const prInfoText = prInfoResult.text()
if (prInfoText.trim()) {
const prInfo = JSON.parse(prInfoText)
// Handle fork PRs
if (prInfo && prInfo.isCrossRepository && prInfo.headRepository && prInfo.headRepositoryOwner) {
const forkOwner = prInfo.headRepositoryOwner.login
const forkName = prInfo.headRepository.name
const remoteName = forkOwner
// Check if remote already exists
const remotes = (await $`git remote`.nothrow().text()).trim()
if (!remotes.split("\n").includes(remoteName)) {
await $`git remote add ${remoteName} https://github.com/${forkOwner}/${forkName}.git`.nothrow()
UI.println(`Added fork remote: ${remoteName}`)
}
// Set upstream to the fork so pushes go there
const headRefName = prInfo.headRefName
await $`git branch --set-upstream-to=${remoteName}/${headRefName} ${localBranchName}`.nothrow()
}
// Check for opencode session link in PR body
if (prInfo && prInfo.body) {
const sessionMatch = prInfo.body.match(/https:\/\/opencode\.ai\/s\/([a-zA-Z0-9_-]+)/)
if (sessionMatch) {
const sessionUrl = sessionMatch[0]
UI.println(`Found opencode session: ${sessionUrl}`)
UI.println(`Importing session...`)
const importResult = await $`opencode import ${sessionUrl}`.nothrow()
if (importResult.exitCode === 0) {
const importOutput = importResult.text().trim()
// Extract session ID from the output (format: "Imported session: <session-id>")
const sessionIdMatch = importOutput.match(/Imported session: ([a-zA-Z0-9_-]+)/)
if (sessionIdMatch) {
sessionId = sessionIdMatch[1]
UI.println(`Session imported: ${sessionId}`)
}
}
}
}
}
}
UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`)
UI.println()
UI.println("Starting opencode...")
UI.println()
// Launch opencode TUI with session ID if available
const { spawn } = await import("child_process")
const opencodeArgs = sessionId ? ["-s", sessionId] : []
const opencodeProcess = spawn("opencode", opencodeArgs, {
stdio: "inherit",
cwd: process.cwd(),
})
await new Promise<void>((resolve, reject) => {
opencodeProcess.on("exit", (code) => {
if (code === 0) resolve()
else reject(new Error(`opencode exited with code ${code}`))
})
opencodeProcess.on("error", reject)
})
},
})
},
})
+11
View File
@@ -393,6 +393,15 @@ function App() {
})
})
event.on(Installation.Event.Updated.type, (evt) => {
toast.show({
variant: "success",
title: "Update Complete",
message: `OpenCode updated to v${evt.properties.version}`,
duration: 5000,
})
})
return (
<box
width={dimensions().width}
@@ -479,6 +488,8 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () =>
)
}
issueURL.searchParams.set("opencode-version", Installation.VERSION)
const copyIssueURL = () => {
Clipboard.copy(issueURL.toString()).then(() => {
setCopied(true)
@@ -8,6 +8,7 @@ import { useSync } from "@tui/context/sync"
import { useTheme } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border"
import { useCommandDialog } from "@tui/component/dialog-command"
import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history"
export type AutocompleteRef = {
@@ -125,10 +126,11 @@ export function Autocomplete(props: {
// Add file options
if (!result.error && result.data) {
const width = store.position.width - 4
options.push(
...result.data.map(
(item): AutocompleteOption => ({
display: item,
display: Locale.truncateMiddle(item, width),
onSelect: () => {
insertPart(item, {
type: "file",
@@ -217,12 +219,6 @@ export function Autocomplete(props: {
description: "compact the session",
onSelect: () => command.trigger("session.compact"),
},
{
display: "/share",
disabled: !!s.share?.url,
description: "share a session",
onSelect: () => command.trigger("session.share"),
},
{
display: "/unshare",
disabled: !s.share,
@@ -250,7 +246,16 @@ export function Autocomplete(props: {
onSelect: () => command.trigger("session.timeline"),
},
)
if (sync.data.config.share !== "disabled") {
results.push({
display: "/share",
disabled: !!s.share?.url,
description: "share a session",
onSelect: () => command.trigger("session.share"),
})
}
}
results.push(
{
display: "/new",
@@ -101,16 +101,81 @@ export function Prompt(props: PromptProps) {
value: "prompt.editor",
onSelect: async (dialog, trigger) => {
dialog.clear()
const value = trigger === "prompt" ? "" : input.plainText
// replace summarized text parts with the actual text
const text = store.prompt.parts
.filter((p) => p.type === "text")
.reduce((acc, p) => {
if (!p.source) return acc
return acc.replace(p.source.text.value, p.text)
}, store.prompt.input)
const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text")
const value = trigger === "prompt" ? "" : text
const content = await Editor.open({ value, renderer })
if (content) {
input.setText(content, { history: false })
setStore("prompt", {
input: content,
parts: [],
if (!content) return
input.setText(content, { history: false })
// Update positions for nonTextParts based on their location in new content
// Filter out parts whose virtual text was deleted
// this handles a case where the user edits the text in the editor
// such that the virtual text moves around or is deleted
const updatedNonTextParts = nonTextParts
.map((part) => {
let virtualText = ""
if (part.type === "file" && part.source?.text) {
virtualText = part.source.text.value
} else if (part.type === "agent" && part.source) {
virtualText = part.source.value
}
if (!virtualText) return part
const newStart = content.indexOf(virtualText)
// if the virtual text is deleted, remove the part
if (newStart === -1) return null
const newEnd = newStart + virtualText.length
if (part.type === "file" && part.source?.text) {
return {
...part,
source: {
...part.source,
text: {
...part.source.text,
start: newStart,
end: newEnd,
},
},
}
}
if (part.type === "agent" && part.source) {
return {
...part,
source: {
...part.source,
start: newStart,
end: newEnd,
},
}
}
return part
})
input.cursorOffset = Bun.stringWidth(content)
}
.filter((part) => part !== null)
setStore("prompt", {
input: content,
// keep only the non-text parts because the text parts were
// already expanded inline
parts: updatedNonTextParts,
})
restoreExtmarksFromParts(updatedNonTextParts)
input.cursorOffset = Bun.stringWidth(content)
},
},
{
@@ -163,11 +228,21 @@ export function Prompt(props: PromptProps) {
if (!props.sessionID) return
if (autocomplete.visible) return
if (!input.focused) return
sdk.client.session.abort({
path: {
id: props.sessionID,
},
})
setStore("interrupt", store.interrupt + 1)
setTimeout(() => {
setStore("interrupt", 0)
}, 5000)
if (store.interrupt >= 2) {
sdk.client.session.abort({
path: {
id: props.sessionID,
},
})
setStore("interrupt", 0)
}
dialog.clear()
},
},
@@ -187,6 +262,7 @@ export function Prompt(props: PromptProps) {
prompt: PromptInfo
mode: "normal" | "shell"
extmarkToPartIndex: Map<number, number>
interrupt: number
}>({
prompt: {
input: "",
@@ -194,6 +270,7 @@ export function Prompt(props: PromptProps) {
},
mode: "normal",
extmarkToPartIndex: new Map(),
interrupt: 0,
})
createEffect(() => {
@@ -681,8 +758,11 @@ export function Prompt(props: PromptProps) {
</Match>
<Match when={status() === "working"}>
<box flexDirection="row" gap={1}>
<text fg={theme.text}>
esc <span style={{ fg: theme.textMuted }}>interrupt</span>
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
esc{" "}
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
</span>
</text>
</box>
</Match>
@@ -90,6 +90,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
},
color(name: string) {
const agent = agents().find((x) => x.name === name)
if (agent?.color) return agent.color
const index = agents().findIndex((x) => x.name === name)
return colors()[index % colors().length]
},
@@ -217,9 +217,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
// blocking
Promise.all([
sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
sdk.client.config.get().then((x) => setStore("config", x.data!)),
sdk.client.config.providers({ throwOnError: true }).then((x) => setStore("provider", x.data!.providers)),
sdk.client.app.agents({ throwOnError: true }).then((x) => setStore("agent", x.data ?? [])),
sdk.client.config.get({ throwOnError: true }).then((x) => setStore("config", x.data!)),
]).then(() => {
setStore("status", "partial")
// non-blocking
@@ -1,5 +1,6 @@
import { SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
import { createMemo } from "solid-js"
import path from "path"
import { createEffect, createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync"
import { createSimpleContext } from "./helper"
import aura from "./theme/aura.json" with { type: "json" }
@@ -27,7 +28,9 @@ import vesper from "./theme/vesper.json" with { type: "json" }
import zenburn from "./theme/zenburn.json" with { type: "json" }
import { useKV } from "./kv"
import { useRenderer } from "@opentui/solid"
import { createStore } from "solid-js/store"
import { createStore, produce } from "solid-js/store"
import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem"
type Theme = {
primary: RGBA
@@ -144,6 +147,17 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
themes: DEFAULT_THEMES,
mode: props.mode,
active: (sync.data.config.theme ?? kv.get("theme", "opencode")) as string,
ready: false,
})
createEffect(async () => {
const custom = await getCustomThemes()
setStore(
produce((draft) => {
Object.assign(draft.themes, custom)
draft.ready = true
}),
)
})
const renderer = useRenderer()
@@ -187,12 +201,39 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
kv.set("theme", theme)
},
get ready() {
return sync.ready
return store.ready
},
}
},
})
const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.json")
async function getCustomThemes() {
const directories = [
Global.Path.config,
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
start: process.cwd(),
}),
)),
]
const result: Record<string, ThemeJson> = {}
for (const dir of directories) {
for await (const item of CUSTOM_THEME_GLOB.scan({
absolute: true,
followSymlinks: true,
dot: true,
cwd: dir,
})) {
const name = path.basename(item, ".json")
result[name] = await Bun.file(item).json()
}
}
return result
}
function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
@@ -3,8 +3,13 @@ import { useSync } from "@tui/context/sync"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route"
import type { PromptInfo } from "@tui/component/prompt/history"
export function DialogMessage(props: { messageID: string; sessionID: string }) {
export function DialogMessage(props: {
messageID: string
sessionID: string
setPrompt?: (prompt: PromptInfo) => void
}) {
const sync = useSync()
const sdk = useSDK()
const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID))
@@ -19,14 +24,33 @@ export function DialogMessage(props: { messageID: string; sessionID: string }) {
value: "session.revert",
description: "undo messages and file changes",
onSelect: (dialog) => {
const msg = message()
if (!msg) return
sdk.client.session.revert({
path: {
id: props.sessionID,
},
body: {
messageID: message()!.id,
messageID: msg.id,
},
})
if (props.setPrompt) {
const parts = sync.data.part[msg.id]
const promptInfo = parts.reduce(
(agg, part) => {
if (part.type === "text") {
if (!part.synthetic) agg.input += part.text
}
if (part.type === "file") agg.parts.push(part)
return agg
},
{ input: "", parts: [] as PromptInfo["parts"] },
)
props.setPrompt(promptInfo)
}
dialog.clear()
},
},
@@ -17,7 +17,7 @@ import { useRoute, useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { SplitBorder } from "@tui/component/border"
import { useTheme } from "@tui/context/theme"
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core"
import { BoxRenderable, ScrollBoxRenderable, TextAttributes, addDefaultParsers } from "@opentui/core"
import { Prompt, type PromptRef } from "@tui/component/prompt"
import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk"
import { useLocal } from "@tui/context/local"
@@ -58,6 +58,7 @@ import { Editor } from "../../util/editor"
import { Global } from "@/global"
import fs from "fs/promises"
import stripAnsi from "strip-ansi"
import { LSP } from "@/lsp/index.ts"
addDefaultParsers(parsers.parsers)
@@ -98,7 +99,7 @@ export function Session() {
await sync.session
.sync(route.sessionID)
.then(() => {
scroll.scrollBy(100_000)
if (scroll) scroll.scrollBy(100_000)
})
.catch(() => {
toast.show({
@@ -145,7 +146,7 @@ export function Session() {
function toBottom() {
setTimeout(() => {
scroll.scrollTo(scroll.scrollHeight)
if (scroll) scroll.scrollTo(scroll.scrollHeight)
}, 50)
}
@@ -216,29 +217,33 @@ export function Session() {
dialog.clear()
},
},
{
title: "Share session",
value: "session.share",
keybind: "session_share",
disabled: !!session()?.share?.url,
category: "Session",
onSelect: async (dialog) => {
await sdk.client.session
.share({
path: {
id: route.sessionID,
...(sync.data.config.share !== "disabled"
? [
{
title: "Share session",
value: "session.share",
keybind: "session_share" as const,
disabled: !!session()?.share?.url,
category: "Session",
onSelect: async (dialog: any) => {
await sdk.client.session
.share({
path: {
id: route.sessionID,
},
})
.then((res) =>
Clipboard.copy(res.data!.share!.url).catch(() =>
toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }),
),
)
.then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
.catch(() => toast.show({ message: "Failed to share session", variant: "error" }))
dialog.clear()
},
})
.then((res) =>
Clipboard.copy(res.data!.share!.url).catch(() =>
toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }),
),
)
.then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
.catch(() => toast.show({ message: "Failed to share session", variant: "error" }))
dialog.clear()
},
},
},
]
: []),
{
title: "Unshare session",
value: "session.unshare",
@@ -752,7 +757,13 @@ export function Session() {
index={index()}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
dialog.replace(() => <DialogMessage messageID={message.id} sessionID={route.sessionID} />)
dialog.replace(() => (
<DialogMessage
messageID={message.id}
sessionID={route.sessionID}
setPrompt={(promptInfo) => prompt.set(promptInfo)}
/>
))
}}
message={message as UserMessage}
parts={sync.data.part[message.id] ?? []}
@@ -950,14 +961,14 @@ const PART_MAPPING = {
function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: AssistantMessage }) {
const { theme, syntax } = useTheme()
const ctx = use()
const content = createMemo(() => props.part.text.trim())
return (
<Show when={props.part.text.trim()}>
<Show when={content()}>
<box
id={"text-" + props.part.id}
paddingLeft={2}
marginTop={1}
flexDirection="row"
gap={1}
flexDirection="column"
border={["left"]}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.backgroundElement}
@@ -967,7 +978,7 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
drawUnstyledText={false}
streaming={true}
syntaxStyle={syntax()}
content={props.part.text.trim()}
content={"_Thinking:_ " + content()}
conceal={ctx.conceal()}
fg={theme.text}
/>
@@ -1201,6 +1212,8 @@ ToolRegistry.register<typeof WriteTool>({
.map((x) => x.toString().padStart(pad, " "))
})
const diagnostics = createMemo(() => props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? [])
return (
<>
<ToolTitle icon="←" fallback="Preparing write..." when={props.input.filePath}>
@@ -1214,6 +1227,15 @@ ToolRegistry.register<typeof WriteTool>({
<code filetype={filetype(props.input.filePath!)} syntaxStyle={syntax()} content={code()} />
</box>
</box>
<Show when={diagnostics().length}>
<For each={diagnostics()}>
{(diagnostic) => (
<text fg={theme.error}>
Error [{diagnostic.range.start.line}:{diagnostic.range.start.character}]: {diagnostic.message}
</text>
)}
</For>
</Show>
</>
)
},
@@ -1391,6 +1413,12 @@ ToolRegistry.register<typeof EditTool>({
const ft = createMemo(() => filetype(props.input.filePath))
createEffect(() => console.log(props.metadata.diagnostics))
const diagnostics = createMemo(() => {
const arr = props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? []
return arr.filter((x) => x.severity === 1).slice(0, 3)
})
return (
<>
<ToolTitle icon="←" fallback="Preparing edit..." when={props.input.filePath}>
@@ -1419,6 +1447,17 @@ ToolRegistry.register<typeof EditTool>({
</box>
</Match>
</Switch>
<Show when={diagnostics().length}>
<box>
<For each={diagnostics()}>
{(diagnostic) => (
<text fg={theme.error}>
Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}] {diagnostic.message}
</text>
)}
</For>
</box>
</Show>
</>
)
},
@@ -1450,15 +1489,24 @@ ToolRegistry.register<typeof TodoWriteTool>({
render(props) {
const { theme } = useTheme()
return (
<box>
<For each={props.input.todos ?? []}>
{(todo) => (
<text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
[{todo.status === "completed" ? "✓" : " "}] {todo.content}
</text>
)}
</For>
</box>
<>
<Show when={!props.input.todos?.length}>
<ToolTitle icon="⚙" fallback="Updating todos..." when={true}>
Updating todos...
</ToolTitle>
</Show>
<Show when={props.metadata.todos?.length}>
<box>
<For each={props.input.todos ?? []}>
{(todo) => (
<text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
[{todo.status === "completed" ? "✓" : " "}] {todo.content}
</text>
)}
</For>
</box>
</Show>
</>
)
},
})
@@ -1,5 +1,5 @@
import { useSync } from "@tui/context/sync"
import { createMemo, For, Show, Switch, Match } from "solid-js"
import { createMemo, For, Show, Switch, Match, createSignal } from "solid-js"
import { useTheme } from "../../context/theme"
import { Locale } from "@/util/locale"
import path from "path"
@@ -13,6 +13,11 @@ export function Sidebar(props: { sessionID: string }) {
const todo = createMemo(() => sync.data.todo[props.sessionID] ?? [])
const messages = createMemo(() => sync.data.message[props.sessionID] ?? [])
const [mcpExpanded, setMcpExpanded] = createSignal(true)
const [diffExpanded, setDiffExpanded] = createSignal(true)
const [todoExpanded, setTodoExpanded] = createSignal(true)
const [lspExpanded, setLspExpanded] = createSignal(true)
const cost = createMemo(() => {
const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
return new Intl.NumberFormat("en-US", {
@@ -55,110 +60,130 @@ export function Sidebar(props: { sessionID: string }) {
</box>
<Show when={Object.keys(sync.data.mcp).length > 0}>
<box>
<text fg={theme.text}>
<b>MCP</b>
</text>
<For each={Object.entries(sync.data.mcp)}>
{([key, item]) => (
<box flexDirection="row" gap={1}>
<text
flexShrink={0}
style={{
fg: {
connected: theme.success,
failed: theme.error,
disabled: theme.textMuted,
}[item.status],
}}
>
</text>
<text fg={theme.text} wrapMode="word">
{key}{" "}
<span style={{ fg: theme.textMuted }}>
<Switch>
<Match when={item.status === "connected"}>Connected</Match>
<Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
<Match when={item.status === "disabled"}>Disabled in configuration</Match>
</Switch>
</span>
</text>
</box>
)}
</For>
<box flexDirection="row" gap={1} onMouseDown={() => setMcpExpanded(!mcpExpanded())}>
<text fg={theme.text}>{mcpExpanded() ? "▼" : "▶"}</text>
<text fg={theme.text}>
<b>MCP</b>
</text>
</box>
<Show when={mcpExpanded()}>
<For each={Object.entries(sync.data.mcp)}>
{([key, item]) => (
<box flexDirection="row" gap={1}>
<text
flexShrink={0}
style={{
fg: {
connected: theme.success,
failed: theme.error,
disabled: theme.textMuted,
}[item.status],
}}
>
</text>
<text fg={theme.text} wrapMode="word">
{key}{" "}
<span style={{ fg: theme.textMuted }}>
<Switch>
<Match when={item.status === "connected"}>Connected</Match>
<Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
<Match when={item.status === "disabled"}>Disabled in configuration</Match>
</Switch>
</span>
</text>
</box>
)}
</For>
</Show>
</box>
</Show>
<Show when={sync.data.lsp.length > 0}>
<box>
<text fg={theme.text}>
<b>LSP</b>
</text>
<For each={sync.data.lsp}>
{(item) => (
<box flexDirection="row" gap={1}>
<text
flexShrink={0}
style={{
fg: {
connected: theme.success,
error: theme.error,
}[item.status],
}}
>
</text>
<text fg={theme.textMuted}>
{item.id} {item.root}
</text>
</box>
)}
</For>
</box>
</Show>
<Show when={diff().length > 0}>
<box>
<text fg={theme.text}>
<b>Modified Files</b>
</text>
<For each={diff() || []}>
{(item) => {
const file = createMemo(() => {
const splits = item.file.split(path.sep).filter(Boolean)
const last = splits.at(-1)!
const rest = splits.slice(0, -1).join(path.sep)
return Locale.truncateMiddle(rest, 30 - last.length) + "/" + last
})
return (
<box flexDirection="row" gap={1} justifyContent="space-between">
<text fg={theme.textMuted} wrapMode="char">
{file()}
<box flexDirection="row" gap={1} onMouseDown={() => setLspExpanded(!lspExpanded())}>
<text fg={theme.text}>{lspExpanded() ? "▼" : "▶"}</text>
<text fg={theme.text}>
<b>LSP</b>
</text>
</box>
<Show when={lspExpanded()}>
<For each={sync.data.lsp}>
{(item) => (
<box flexDirection="row" gap={1}>
<text
flexShrink={0}
style={{
fg: {
connected: theme.success,
error: theme.error,
}[item.status],
}}
>
</text>
<text fg={theme.textMuted}>
{item.id} {item.root}
</text>
<box flexDirection="row" gap={1} flexShrink={0}>
<Show when={item.additions}>
<text fg={theme.diffAdded}>+{item.additions}</text>
</Show>
<Show when={item.deletions}>
<text fg={theme.diffRemoved}>-{item.deletions}</text>
</Show>
</box>
</box>
)
}}
</For>
)}
</For>
</Show>
</box>
</Show>
<Show when={todo().length > 0}>
<box>
<text fg={theme.text}>
<b>Todo</b>
</text>
<For each={todo()}>
{(todo) => (
<text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
[{todo.status === "completed" ? "✓" : " "}] {todo.content}
</text>
)}
</For>
<box flexDirection="row" gap={1} onMouseDown={() => setTodoExpanded(!todoExpanded())}>
<text fg={theme.text}>{todoExpanded() ? "▼" : "▶"}</text>
<text fg={theme.text}>
<b>Todo</b>
</text>
</box>
<Show when={todoExpanded()}>
<For each={todo()}>
{(todo) => (
<text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
[{todo.status === "completed" ? "✓" : " "}] {todo.content}
</text>
)}
</For>
</Show>
</box>
</Show>
<Show when={diff().length > 0}>
<box>
<box flexDirection="row" gap={1} onMouseDown={() => setDiffExpanded(!diffExpanded())}>
<text fg={theme.text}>{diffExpanded() ? "▼" : "▶"}</text>
<text fg={theme.text}>
<b>Modified Files</b>
</text>
</box>
<Show when={diffExpanded()}>
<For each={diff() || []}>
{(item) => {
const file = createMemo(() => {
const splits = item.file.split(path.sep).filter(Boolean)
const last = splits.at(-1)!
const rest = splits.slice(0, -1).join(path.sep)
return Locale.truncateMiddle(rest, 30 - last.length) + "/" + last
})
return (
<box flexDirection="row" gap={1} justifyContent="space-between">
<text fg={theme.textMuted} wrapMode="char">
{file()}
</text>
<box flexDirection="row" gap={1} flexShrink={0}>
<Show when={item.additions}>
<text fg={theme.diffAdded}>+{item.additions}</text>
</Show>
<Show when={item.deletions}>
<text fg={theme.diffRemoved}>-{item.deletions}</text>
</Show>
</box>
</box>
)
}}
</For>
</Show>
</box>
</Show>
</box>
@@ -48,6 +48,10 @@ export const TuiSpawnCommand = cmd({
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
env: {
...process.env,
BUN_OPTIONS: "",
},
})
await proc.exited
await Instance.disposeAll()
@@ -259,10 +259,15 @@ function Option(props: {
onMouseOver?: () => void
}) {
const { theme } = useTheme()
return (
<>
<Show when={props.current && !props.active}>
<text flexShrink={0} fg={theme.primary} marginRight={0.5}>
<Show when={props.current}>
<text
flexShrink={0}
fg={props.active ? theme.background : props.current ? theme.primary : theme.text}
marginRight={0.5}
>
</text>
</Show>
@@ -1,5 +1,5 @@
import { $ } from "bun"
import { platform } from "os"
import { platform, release } from "os"
import clipboardy from "clipboardy"
import { lazy } from "../../../../util/lazy.js"
import { tmpdir } from "os"
@@ -29,6 +29,18 @@ export namespace Clipboard {
}
}
if (os === "win32" || release().includes("WSL")) {
const script =
"Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
const base64 = await $`powershell.exe -command "${script}"`.nothrow().text()
if (base64) {
const imageBuffer = Buffer.from(base64.trim(), "base64")
if (imageBuffer.length > 0) {
return { data: imageBuffer.toString("base64"), mime: "image/png" }
}
}
}
if (os === "linux") {
const wayland = await $`wl-paste -t image/png`.nothrow().arrayBuffer()
if (wayland && wayland.byteLength > 0) {
@@ -40,18 +52,6 @@ export namespace Clipboard {
}
}
if (os === "win32") {
const script =
"Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
const base64 = await $`powershell -command "${script}"`.nothrow().text()
if (base64) {
const imageBuffer = Buffer.from(base64.trim(), "base64")
if (imageBuffer.length > 0) {
return { data: imageBuffer.toString("base64"), mime: "image/png" }
}
}
}
const text = await clipboardy.read().catch(() => {})
if (text) {
return { data: text, mime: "text/plain" }
+21 -12
View File
@@ -24,12 +24,6 @@ export namespace Config {
export const state = Instance.state(async () => {
const auth = await Auth.all()
let result = await global()
for (const file of ["opencode.jsonc", "opencode.json"]) {
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
for (const resolved of found.toReversed()) {
result = mergeDeep(result, await loadFile(resolved))
}
}
// Override with custom config if provided
if (Flag.OPENCODE_CONFIG) {
@@ -37,6 +31,13 @@ export namespace Config {
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
for (const file of ["opencode.jsonc", "opencode.json"]) {
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
for (const resolved of found.toReversed()) {
result = mergeDeep(result, await loadFile(resolved))
}
}
if (Flag.OPENCODE_CONFIG_CONTENT) {
result = mergeDeep(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
@@ -74,12 +75,15 @@ export namespace Config {
for (const dir of directories) {
await assertValid(dir)
for (const file of ["opencode.jsonc", "opencode.json"]) {
result = mergeDeep(result, await loadFile(path.join(dir, file)))
// to satisy the type checker
result.agent ??= {}
result.mode ??= {}
result.plugin ??= []
if (dir.endsWith(".opencode")) {
for (const file of ["opencode.jsonc", "opencode.json"]) {
log.debug(`loading config from ${path.join(dir, file)}`)
result = mergeDeep(result, await loadFile(path.join(dir, file)))
// to satisy the type checker
result.agent ??= {}
result.mode ??= {}
result.plugin ??= []
}
}
promises.push(installDependencies(dir))
@@ -355,6 +359,11 @@ export namespace Config {
disable: z.boolean().optional(),
description: z.string().optional().describe("Description of when to use the agent"),
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
color: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")
.optional()
.describe("Hex color code for the agent (e.g., #FF5733)"),
permission: z
.object({
edit: Permission.optional(),
+3 -3
View File
@@ -13,9 +13,9 @@ export namespace Flag {
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
// Experimental
export const OPENCODE_EXPERIMENTAL_WATCHER = truthy("OPENCODE_EXPERIMENTAL_WATCHER")
export const OPENCODE_EXPERIMENTAL_TURN_SUMMARY = truthy("OPENCODE_EXPERIMENTAL_TURN_SUMMARY")
export const OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP = truthy("OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP")
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
export const OPENCODE_EXPERIMENTAL_WATCHER = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WATCHER")
export const OPENCODE_EXPERIMENTAL_EXA = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA")
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
+2
View File
@@ -24,6 +24,7 @@ import { TuiSpawnCommand } from "./cli/cmd/tui/spawn"
import { AcpCommand } from "./cli/cmd/acp"
import { EOL } from "os"
import { WebCommand } from "./cli/cmd/web"
import { PrCommand } from "./cli/cmd/pr"
process.on("unhandledRejection", (e) => {
Log.Default.error("rejection", {
@@ -90,6 +91,7 @@ const cli = yargs(hideBin(process.argv))
.command(ExportCommand)
.command(ImportCommand)
.command(GithubCommand)
.command(PrCommand)
.fail((msg) => {
if (
msg.startsWith("Unknown argument") ||
+65 -28
View File
@@ -103,6 +103,7 @@ export namespace LSP {
broken: new Set<string>(),
servers,
clients,
spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
}
},
async (state) => {
@@ -145,6 +146,49 @@ export namespace LSP {
const s = await state()
const extension = path.parse(file).ext || file
const result: LSPClient.Info[] = []
async function schedule(server: LSPServer.Info, root: string, key: string) {
const handle = await server
.spawn(root)
.then((value) => {
if (!value) s.broken.add(key)
return value
})
.catch((err) => {
s.broken.add(key)
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
return undefined
})
if (!handle) return undefined
log.info("spawned lsp server", { serverID: server.id })
const client = await LSPClient.create({
serverID: server.id,
server: handle,
root,
}).catch((err) => {
s.broken.add(key)
handle.process.kill()
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
return undefined
})
if (!client) {
handle.process.kill()
return undefined
}
const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
if (existing) {
handle.process.kill()
return existing
}
s.clients.push(client)
return client
}
for (const server of Object.values(s.servers)) {
if (server.extensions.length && !server.extensions.includes(extension)) continue
const root = await server.root(file)
@@ -156,49 +200,42 @@ export namespace LSP {
result.push(match)
continue
}
const handle = await server
.spawn(root)
.then((h) => {
if (h === undefined) {
s.broken.add(root + server.id)
}
return h
})
.catch((err) => {
s.broken.add(root + server.id)
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
return undefined
})
if (!handle) continue
log.info("spawned lsp server", { serverID: server.id })
const client = await LSPClient.create({
serverID: server.id,
server: handle,
root,
}).catch((err) => {
s.broken.add(root + server.id)
handle.process.kill()
log.error(`Failed to initialize LSP client ${server.id}`, {
error: err,
})
return undefined
const inflight = s.spawning.get(root + server.id)
if (inflight) {
const client = await inflight
if (!client) continue
result.push(client)
continue
}
const task = schedule(server, root, root + server.id)
s.spawning.set(root + server.id, task)
task.finally(() => {
if (s.spawning.get(root + server.id) === task) {
s.spawning.delete(root + server.id)
}
})
const client = await task
if (!client) continue
s.clients.push(client)
result.push(client)
Bus.publish(Event.Updated, {})
}
return result
}
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
log.info("touching file", { file: input })
const clients = await getClients(input)
await run(async (client) => {
if (!clients.includes(client)) return
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
await client.notify.open({ path: input })
return wait
}).catch((err) => {
log.error("failed to touch file", { err, file: input })
+159 -63
View File
@@ -547,6 +547,40 @@ export namespace LSPServer {
},
}
export const SourceKit: Info = {
id: "sourcekit-lsp",
extensions: [".swift", ".objc", "objcpp"],
root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]),
async spawn(root) {
// Check if sourcekit-lsp is available in the PATH
// This is installed with the Swift toolchain
const sourcekit = Bun.which("sourcekit-lsp")
if (sourcekit) {
return {
process: spawn(sourcekit, {
cwd: root,
}),
}
}
// If sourcekit-lsp not found, check if xcrun is available
// This is specific to macOS where sourcekit-lsp is typically installed with Xcode
if (!Bun.which("xcrun")) return
const lspLoc = await $`xcrun --find sourcekit-lsp`.quiet().nothrow()
if (lspLoc.exitCode !== 0) return
const bin = lspLoc.text().trim()
return {
process: spawn(bin, {
cwd: root,
}),
}
},
}
export const RustAnalyzer: Info = {
id: "rust",
root: async (root) => {
@@ -598,73 +632,135 @@ export namespace LSPServer {
root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
async spawn(root) {
let bin = Bun.which("clangd", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading clangd from GitHub releases")
const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
if (!releaseResponse.ok) {
log.error("Failed to fetch clangd release info")
return
const args = ["--background-index", "--clang-tidy"]
const fromPath = Bun.which("clangd")
if (fromPath) {
return {
process: spawn(fromPath, args, {
cwd: root,
}),
}
const release = (await releaseResponse.json()) as any
const platform = process.platform
let assetName = ""
if (platform === "darwin") {
assetName = "clangd-mac-"
} else if (platform === "linux") {
assetName = "clangd-linux-"
} else if (platform === "win32") {
assetName = "clangd-windows-"
} else {
log.error(`Platform ${platform} is not supported by clangd auto-download`)
return
}
assetName += release.tag_name + ".zip"
const asset = release.assets.find((a: any) => a.name === assetName)
if (!asset) {
log.error(`Could not find asset ${assetName} in latest clangd release`)
return
}
const downloadUrl = asset.browser_download_url
const downloadResponse = await fetch(downloadUrl)
if (!downloadResponse.ok) {
log.error("Failed to download clangd")
return
}
const zipPath = path.join(Global.Path.bin, "clangd.zip")
await Bun.file(zipPath).write(downloadResponse)
await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
await fs.rm(zipPath, { force: true })
const extractedDir = path.join(Global.Path.bin, assetName.replace(".zip", ""))
bin = path.join(extractedDir, "bin", "clangd" + (platform === "win32" ? ".exe" : ""))
if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract clangd binary")
return
}
if (platform !== "win32") {
await $`chmod +x ${bin}`.nothrow()
}
log.info(`installed clangd`, { bin })
}
const ext = process.platform === "win32" ? ".exe" : ""
const direct = path.join(Global.Path.bin, "clangd" + ext)
if (await Bun.file(direct).exists()) {
return {
process: spawn(direct, args, {
cwd: root,
}),
}
}
const entries = await fs.readdir(Global.Path.bin, { withFileTypes: true }).catch(() => [])
for (const entry of entries) {
if (!entry.isDirectory()) continue
if (!entry.name.startsWith("clangd_")) continue
const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
if (await Bun.file(candidate).exists()) {
return {
process: spawn(candidate, args, {
cwd: root,
}),
}
}
}
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading clangd from GitHub releases")
const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
if (!releaseResponse.ok) {
log.error("Failed to fetch clangd release info")
return
}
const release: {
tag_name?: string
assets?: { name?: string; browser_download_url?: string }[]
} = await releaseResponse.json()
const tag = release.tag_name
if (!tag) {
log.error("clangd release did not include a tag name")
return
}
const platform = process.platform
const tokens: Record<string, string> = {
darwin: "mac",
linux: "linux",
win32: "windows",
}
const token = tokens[platform]
if (!token) {
log.error(`Platform ${platform} is not supported by clangd auto-download`)
return
}
const assets = release.assets ?? []
const valid = (item: { name?: string; browser_download_url?: string }) => {
if (!item.name) return false
if (!item.browser_download_url) return false
if (!item.name.includes(token)) return false
return item.name.includes(tag)
}
const asset =
assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ??
assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ??
assets.find((item) => valid(item))
if (!asset?.name || !asset.browser_download_url) {
log.error("clangd could not match release asset", { tag, platform })
return
}
const name = asset.name
const downloadResponse = await fetch(asset.browser_download_url)
if (!downloadResponse.ok) {
log.error("Failed to download clangd")
return
}
const archive = path.join(Global.Path.bin, name)
const buf = await downloadResponse.arrayBuffer()
if (buf.byteLength === 0) {
log.error("Failed to write clangd archive")
return
}
await Bun.write(archive, buf)
const zip = name.endsWith(".zip")
const tar = name.endsWith(".tar.xz")
if (!zip && !tar) {
log.error("clangd encountered unsupported asset", { asset: name })
return
}
if (zip) {
await $`unzip -o -q ${archive}`.quiet().cwd(Global.Path.bin).nothrow()
}
if (tar) {
await $`tar -xf ${archive}`.cwd(Global.Path.bin).nothrow()
}
await fs.rm(archive, { force: true })
const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract clangd binary")
return
}
if (platform !== "win32") {
await $`chmod +x ${bin}`.nothrow()
}
await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {})
log.info(`installed clangd`, { bin })
return {
process: spawn(bin, ["--background-index", "--clang-tidy"], {
process: spawn(bin, args, {
cwd: root,
}),
}
+1 -1
View File
@@ -28,7 +28,7 @@ export namespace Plugin {
}
const plugins = [...(config.plugin ?? [])]
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
plugins.push("opencode-copilot-auth@0.0.4")
plugins.push("opencode-copilot-auth@0.0.5")
plugins.push("opencode-anthropic-auth@0.0.2")
}
for (let plugin of plugins) {
@@ -12,7 +12,6 @@ import { Instance } from "./instance"
import { Log } from "@/util/log"
export async function InstanceBootstrap() {
if (Flag.OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP) return
Log.Default.info("bootstrapping", { directory: Instance.directory })
await Plugin.init()
Share.init()
+21 -11
View File
@@ -41,18 +41,28 @@ export namespace Project {
return project
}
let worktree = path.dirname(git)
const [id] = await $`git rev-list --max-parents=0 --all`
.quiet()
.nothrow()
.cwd(worktree)
const timer = log.time("git.rev-parse")
let id = await Bun.file(path.join(git, "opencode"))
.text()
.then((x) =>
x
.split("\n")
.filter(Boolean)
.map((x) => x.trim())
.toSorted(),
)
.then((x) => x.trim())
.catch(() => {})
if (!id) {
const roots = await $`git rev-list --max-parents=0 --all`
.quiet()
.nothrow()
.cwd(worktree)
.text()
.then((x) =>
x
.split("\n")
.filter(Boolean)
.map((x) => x.trim())
.toSorted(),
)
id = roots[0]
if (id) Bun.file(path.join(git, "opencode")).write(id)
}
timer.stop()
if (!id) {
const project: Info = {
id: "global",
+9 -3
View File
@@ -12,6 +12,7 @@ import { Auth } from "../auth"
import { Instance } from "../project/instance"
import { Global } from "../global"
import { Flag } from "../flag/flag"
import { iife } from "@/util/iife"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -193,7 +194,7 @@ export namespace Provider {
},
"google-vertex-anthropic": async () => {
const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "us-east5"
const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "global"
const autoload = Boolean(project)
if (!autoload) return { autoload: false }
return {
@@ -289,10 +290,15 @@ export namespace Provider {
}
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
const existing = parsed.models[modelID]
const existing = parsed.models[model.id ?? modelID]
const name = iife(() => {
if (model.name) return model.name
if (model.id && model.id !== modelID) return modelID
return existing?.name ?? modelID
})
const parsedModel: ModelsDev.Model = {
id: modelID,
name: model.name ?? existing?.name ?? modelID,
name,
release_date: model.release_date ?? existing?.release_date,
attachment: model.attachment ?? existing?.attachment ?? false,
reasoning: model.reasoning ?? existing?.reasoning ?? false,
+1
View File
@@ -163,6 +163,7 @@ export namespace Server {
return c.json(await Config.get())
},
)
.patch(
"/config",
describeRoute({
+11 -2
View File
@@ -378,8 +378,14 @@ export namespace Session {
metadata: z.custom<ProviderMetadata>().optional(),
}),
(input) => {
const cachedInputTokens = input.usage.cachedInputTokens ?? 0
const excludesCachedTokens = !!(input.metadata?.["anthropic"] || input.metadata?.["bedrock"])
const adjustedInputTokens = excludesCachedTokens
? (input.usage.inputTokens ?? 0)
: (input.usage.inputTokens ?? 0) - cachedInputTokens
const tokens = {
input: input.usage.inputTokens ?? 0,
input: adjustedInputTokens,
output: input.usage.outputTokens ?? 0,
reasoning: input.usage?.reasoningTokens ?? 0,
cache: {
@@ -387,7 +393,7 @@ export namespace Session {
// @ts-expect-error
input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
0) as number,
read: input.usage.cachedInputTokens ?? 0,
read: cachedInputTokens,
},
}
return {
@@ -396,6 +402,9 @@ export namespace Session {
.add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000))
// TODO: update models.dev to have better pricing model, for now:
// charge reasoning tokens at the same rate as output tokens
.add(new Decimal(tokens.reasoning).mul(input.model.cost?.output ?? 0).div(1_000_000))
.toNumber(),
tokens,
}
+49 -45
View File
@@ -145,6 +145,54 @@ export namespace SessionPrompt {
),
})
export type PromptInput = z.infer<typeof PromptInput>
export async function resolvePromptParts(template: string): Promise<PromptInput["parts"]> {
const parts: PromptInput["parts"] = [
{
type: "text",
text: template,
},
]
const files = ConfigMarkdown.files(template)
await Promise.all(
files.map(async (match) => {
const name = match[1]
const filepath = name.startsWith("~/")
? path.join(os.homedir(), name.slice(2))
: path.resolve(Instance.worktree, name)
const stats = await fs.stat(filepath).catch(() => undefined)
if (!stats) {
const agent = await Agent.get(name)
if (agent) {
parts.push({
type: "agent",
name: agent.name,
})
}
return
}
if (stats.isDirectory()) {
parts.push({
type: "file",
url: `file://${filepath}`,
filename: name,
mime: "application/x-directory",
})
return
}
parts.push({
type: "file",
url: `file://${filepath}`,
filename: name,
mime: "text/plain",
})
}),
)
return parts
}
export async function prompt(input: PromptInput): Promise<MessageV2.WithParts> {
const l = log.clone().tag("session", input.sessionID)
l.info("prompt")
@@ -1605,51 +1653,7 @@ export namespace SessionPrompt {
}
template = template.trim()
const parts = [
{
type: "text",
text: template,
},
] as PromptInput["parts"]
const files = ConfigMarkdown.files(template)
await Promise.all(
files.map(async (match) => {
const name = match[1]
const filepath = name.startsWith("~/")
? path.join(os.homedir(), name.slice(2))
: path.resolve(Instance.worktree, name)
const stats = await fs.stat(filepath).catch(() => undefined)
if (!stats) {
const agent = await Agent.get(name)
if (agent) {
parts.push({
type: "agent",
name: agent.name,
})
}
return
}
if (stats.isDirectory()) {
parts.push({
type: "file",
url: `file://${filepath}`,
filename: name,
mime: "application/x-directory",
})
return
}
parts.push({
type: "file",
url: `file://${filepath}`,
filename: name,
mime: "text/plain",
})
}),
)
const parts = await resolvePromptParts(template)
const model = await (async () => {
if (command.model) {
+4 -3
View File
@@ -170,7 +170,8 @@ export namespace Storage {
const target = path.join(dir, ...key) + ".json"
return withErrorHandling(async () => {
using _ = await Lock.read(target)
return Bun.file(target).json() as Promise<T>
const result = await Bun.file(target).json()
return result as T
})
}
@@ -178,7 +179,7 @@ export namespace Storage {
const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json"
return withErrorHandling(async () => {
using _ = await Lock.write("storage")
using _ = await Lock.write(target)
const content = await Bun.file(target).json()
fn(content)
await Bun.write(target, JSON.stringify(content, null, 2))
@@ -190,7 +191,7 @@ export namespace Storage {
const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json"
return withErrorHandling(async () => {
using _ = await Lock.write("storage")
using _ = await Lock.write(target)
await Bun.write(target, JSON.stringify(content, null, 2))
})
}
+138
View File
@@ -0,0 +1,138 @@
import z from "zod"
import { Tool } from "./tool"
import DESCRIPTION from "./codesearch.txt"
import { Config } from "../config/config"
import { Permission } from "../permission"
const API_CONFIG = {
BASE_URL: "https://mcp.exa.ai",
ENDPOINTS: {
CONTEXT: "/mcp",
},
} as const
interface McpCodeRequest {
jsonrpc: string
id: number
method: string
params: {
name: string
arguments: {
query: string
tokensNum: number
}
}
}
interface McpCodeResponse {
jsonrpc: string
result: {
content: Array<{
type: string
text: string
}>
}
}
export const CodeSearchTool = Tool.define("codesearch", {
description: DESCRIPTION,
parameters: z.object({
query: z
.string()
.describe(
"Search query to find relevant context for APIs, Libraries, and SDKs. For example, 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Next js partial prerendering configuration'",
),
tokensNum: z
.number()
.min(1000)
.max(50000)
.default(5000)
.describe(
"Number of tokens to return (1000-50000). Default is 5000 tokens. Adjust this value based on how much context you need - use lower values for focused queries and higher values for comprehensive documentation.",
),
}),
async execute(params, ctx) {
const cfg = await Config.get()
if (cfg.permission?.webfetch === "ask")
await Permission.ask({
type: "codesearch",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: "Search code for: " + params.query,
metadata: {
query: params.query,
tokensNum: params.tokensNum,
},
})
const codeRequest: McpCodeRequest = {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "get_code_context_exa",
arguments: {
query: params.query,
tokensNum: params.tokensNum || 5000,
},
},
}
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 30000)
try {
const headers: Record<string, string> = {
accept: "application/json, text/event-stream",
"content-type": "application/json",
}
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.CONTEXT}`, {
method: "POST",
headers,
body: JSON.stringify(codeRequest),
signal: AbortSignal.any([controller.signal, ctx.abort]),
})
clearTimeout(timeoutId)
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Code search error (${response.status}): ${errorText}`)
}
const responseText = await response.text()
// Parse SSE response
const lines = responseText.split("\n")
for (const line of lines) {
if (line.startsWith("data: ")) {
const data: McpCodeResponse = JSON.parse(line.substring(6))
if (data.result && data.result.content && data.result.content.length > 0) {
return {
output: data.result.content[0].text,
title: `Code search: ${params.query}`,
metadata: {},
}
}
}
}
return {
output:
"No code snippets or documentation found. Please try a different query, be more specific about the library or programming concept, or check the spelling of framework names.",
title: `Code search: ${params.query}`,
metadata: {},
}
} catch (error) {
clearTimeout(timeoutId)
if (error instanceof Error && error.name === "AbortError") {
throw new Error("Code search request timed out")
}
throw error
}
},
})
+12
View File
@@ -0,0 +1,12 @@
- Search and get relevant context for any programming task using Exa Code API
- Provides the highest quality and freshest context for libraries, SDKs, and APIs
- Use this tool for ANY question or task related to programming
- Returns comprehensive code examples, documentation, and API references
- Optimized for finding specific programming patterns and solutions
Usage notes:
- Adjustable token count (1000-50000) for focused or comprehensive results
- Default 5000 tokens provides balanced context for most queries
- Use lower values for specific questions, higher values for comprehensive documentation
- Supports queries about frameworks, libraries, APIs, and programming concepts
- Examples: 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware'
+4
View File
@@ -17,6 +17,9 @@ import path from "path"
import { type ToolDefinition } from "@opencode-ai/plugin"
import z from "zod"
import { Plugin } from "../plugin"
import { WebSearchTool } from "./websearch"
import { CodeSearchTool } from "./codesearch"
import { Flag } from "@/flag/flag"
export namespace ToolRegistry {
export const state = Instance.state(async () => {
@@ -91,6 +94,7 @@ export namespace ToolRegistry {
TodoWriteTool,
TodoReadTool,
TaskTool,
...(Flag.OPENCODE_EXPERIMENTAL_EXA ? [WebSearchTool, CodeSearchTool] : []),
...custom,
]
}
+2 -7
View File
@@ -65,6 +65,7 @@ export const TaskTool = Tool.define("task", async () => {
ctx.abort.addEventListener("abort", () => {
SessionLock.abort(session.id)
})
const promptParts = await SessionPrompt.resolvePromptParts(params.prompt)
const result = await SessionPrompt.prompt({
messageID,
sessionID: session.id,
@@ -79,13 +80,7 @@ export const TaskTool = Tool.define("task", async () => {
task: false,
...agent.tools,
},
parts: [
{
id: Identifier.ascending("part"),
type: "text",
text: params.prompt,
},
],
parts: promptParts,
})
unsub()
let all
+150
View File
@@ -0,0 +1,150 @@
import z from "zod"
import { Tool } from "./tool"
import DESCRIPTION from "./websearch.txt"
import { Config } from "../config/config"
import { Permission } from "../permission"
const API_CONFIG = {
BASE_URL: "https://mcp.exa.ai",
ENDPOINTS: {
SEARCH: "/mcp",
},
DEFAULT_NUM_RESULTS: 8,
} as const
interface McpSearchRequest {
jsonrpc: string
id: number
method: string
params: {
name: string
arguments: {
query: string
numResults?: number
livecrawl?: "fallback" | "preferred"
type?: "auto" | "fast" | "deep"
contextMaxCharacters?: number
}
}
}
interface McpSearchResponse {
jsonrpc: string
result: {
content: Array<{
type: string
text: string
}>
}
}
export const WebSearchTool = Tool.define("websearch", {
description: DESCRIPTION,
parameters: z.object({
query: z.string().describe("Websearch query"),
numResults: z.number().optional().describe("Number of search results to return (default: 8)"),
livecrawl: z
.enum(["fallback", "preferred"])
.optional()
.describe(
"Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
),
type: z
.enum(["auto", "fast", "deep"])
.optional()
.describe("Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search"),
contextMaxCharacters: z
.number()
.optional()
.describe("Maximum characters for context string optimized for LLMs (default: 10000)"),
}),
async execute(params, ctx) {
const cfg = await Config.get()
if (cfg.permission?.webfetch === "ask")
await Permission.ask({
type: "websearch",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: "Search web for: " + params.query,
metadata: {
query: params.query,
numResults: params.numResults,
livecrawl: params.livecrawl,
type: params.type,
contextMaxCharacters: params.contextMaxCharacters,
},
})
const searchRequest: McpSearchRequest = {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "web_search_exa",
arguments: {
query: params.query,
type: params.type || "auto",
numResults: params.numResults || API_CONFIG.DEFAULT_NUM_RESULTS,
livecrawl: params.livecrawl || "fallback",
contextMaxCharacters: params.contextMaxCharacters,
},
},
}
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 25000)
try {
const headers: Record<string, string> = {
accept: "application/json, text/event-stream",
"content-type": "application/json",
}
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SEARCH}`, {
method: "POST",
headers,
body: JSON.stringify(searchRequest),
signal: AbortSignal.any([controller.signal, ctx.abort]),
})
clearTimeout(timeoutId)
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Search error (${response.status}): ${errorText}`)
}
const responseText = await response.text()
// Parse SSE response
const lines = responseText.split("\n")
for (const line of lines) {
if (line.startsWith("data: ")) {
const data: McpSearchResponse = JSON.parse(line.substring(6))
if (data.result && data.result.content && data.result.content.length > 0) {
return {
output: data.result.content[0].text,
title: `Web search: ${params.query}`,
metadata: {},
}
}
}
}
return {
output: "No search results found. Please try a different query.",
title: `Web search: ${params.query}`,
metadata: {},
}
} catch (error) {
clearTimeout(timeoutId)
if (error instanceof Error && error.name === "AbortError") {
throw new Error("Search request timed out")
}
throw error
}
},
})
+7 -7
View File
@@ -1,11 +1,11 @@
- Allows opencode to search the web and use the results to inform responses
- Search the web using Exa AI - performs real-time web searches and can scrape content from specific URLs
- Provides up-to-date information for current events and recent data
- Returns search result information formatted as search result blocks
- Use this tool for accessing information beyond Claude's knowledge cutoff
- Supports configurable result counts and returns the content from the most relevant websites
- Use this tool for accessing information beyond knowledge cutoff
- Searches are performed automatically within a single API call
Usage notes:
- Domain filtering is supported to include or block specific websites
- Web search is only available in the US
- Supports live crawling modes: 'fallback' (backup if cached unavailable) or 'preferred' (prioritize live crawling)
- Search types: 'auto' (balanced), 'fast' (quick results), 'deep' (comprehensive search)
- Configurable context length for optimal LLM integration
- Domain filtering and advanced search options available
+19
View File
@@ -0,0 +1,19 @@
export namespace Color {
export function isValidHex(hex?: string): hex is string {
if (!hex) return false
return /^#[0-9a-fA-F]{6}$/.test(hex)
}
export function hexToRgb(hex: string): { r: number; g: number; b: number } {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return { r, g, b }
}
export function hexToAnsiBold(hex?: string): string | undefined {
if (!isValidHex(hex)) return undefined
const { r, g, b } = hexToRgb(hex)
return `\x1b[38;2;${r};${g};${b}m\x1b[1m`
}
}
@@ -0,0 +1,66 @@
import { test, expect } from "bun:test"
import path from "path"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Config } from "../../src/config/config"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { Color } from "../../src/util/color"
test("agent color parsed from project config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
agent: {
build: { color: "#FFA500" },
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const cfg = await Config.get()
expect(cfg.agent?.["build"]?.color).toBe("#FFA500")
},
})
})
test("Agent.get includes color from config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
agent: {
plan: { color: "#A855F7" },
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const plan = await AgentSvc.get("plan")
expect(plan?.color).toBe("#A855F7")
},
})
})
test("Color.hexToAnsiBold converts valid hex to ANSI", () => {
const result = Color.hexToAnsiBold("#FFA500")
expect(result).toBe("\x1b[38;2;255;165;0m\x1b[1m")
})
test("Color.hexToAnsiBold returns undefined for invalid hex", () => {
expect(Color.hexToAnsiBold(undefined)).toBeUndefined()
expect(Color.hexToAnsiBold("")).toBeUndefined()
expect(Color.hexToAnsiBold("#FFF")).toBeUndefined()
expect(Color.hexToAnsiBold("FFA500")).toBeUndefined()
expect(Color.hexToAnsiBold("#GGGGGG")).toBeUndefined()
})
@@ -0,0 +1,40 @@
import { describe, expect, test } from "bun:test"
import { Project } from "../../src/project/project"
import { Log } from "../../src/util/log"
import { $ } from "bun"
import path from "path"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
describe("Project.fromDirectory", () => {
test("should handle git repository with no commits", async () => {
await using tmp = await tmpdir()
await $`git init`.cwd(tmp.path).quiet()
const project = await Project.fromDirectory(tmp.path)
expect(project).toBeDefined()
expect(project.id).toBe("global")
expect(project.worktree).toBe("/")
const opencodeFile = path.join(tmp.path, ".git", "opencode")
const fileExists = await Bun.file(opencodeFile).exists()
expect(fileExists).toBe(false)
})
test("should handle git repository with commits", async () => {
await using tmp = await tmpdir({ git: true })
const project = await Project.fromDirectory(tmp.path)
expect(project).toBeDefined()
expect(project.id).not.toBe("global")
expect(project.vcs).toBe("git")
expect(project.worktree).toBe(tmp.path)
const opencodeFile = path.join(tmp.path, ".git", "opencode")
const fileExists = await Bun.file(opencodeFile).exists()
expect(fileExists).toBe(true)
})
})
+1 -1
View File
@@ -48,7 +48,7 @@ describe("tool.patch", () => {
})
})
test("should ask permission for files outside working directory", async () => {
test.skip("should ask permission for files outside working directory", async () => {
await Instance.provide({
directory: "/tmp",
fn: async () => {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.55",
"version": "1.0.62",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+2
View File
@@ -49,6 +49,7 @@ type Agent struct {
Options map[string]interface{} `json:"options,required"`
Permission AgentPermission `json:"permission,required"`
Tools map[string]bool `json:"tools,required"`
Color string `json:"color"`
Description string `json:"description"`
Model AgentModel `json:"model"`
Prompt string `json:"prompt"`
@@ -65,6 +66,7 @@ type agentJSON struct {
Options apijson.Field
Permission apijson.Field
Tools apijson.Field
Color apijson.Field
Description apijson.Field
Model apijson.Field
Prompt apijson.Field
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.55",
"version": "1.0.62",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+5
View File
@@ -190,6 +190,10 @@ export type AgentConfig = {
*/
description?: string
mode?: "subagent" | "primary" | "all"
/**
* Hex color code for the agent (e.g., #FF5733)
*/
color?: string
permission?: {
edit?: "ask" | "allow" | "deny"
bash?:
@@ -1043,6 +1047,7 @@ export type Agent = {
builtIn: boolean
topP?: number
temperature?: number
color?: string
permission: {
edit: "ask" | "allow" | "deny"
bash: {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.55",
"version": "1.0.62",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.55",
"version": "1.0.62",
"type": "module",
"exports": {
".": "./src/components/index.ts",
+23 -15
View File
@@ -12,9 +12,6 @@
align-items: flex-start;
gap: 0px;
align-self: stretch;
border: 1px solid var(--border-weak-base);
border-bottom: none;
border-top: none;
overflow: clip;
[data-slot="accordion-header"] {
@@ -36,7 +33,7 @@
user-select: none;
background-color: var(--surface-base);
border-bottom: 1px solid var(--border-weak-base);
border: 1px solid var(--border-weak-base);
overflow: clip;
color: var(--text-strong);
transition: background-color 0.15s ease;
@@ -62,11 +59,19 @@
}
&[data-expanded] {
border: 1px solid var(--border-weak-base);
border-bottom: 1px solid var(--border-weak-base);
margin-top: 8px;
margin-bottom: 8px;
border-radius: 8px;
[data-slot="accordion-trigger"] {
border-radius: 8px 8px 0 0;
}
[data-slot="accordion-content"] {
border: 1px solid var(--border-weak-base);
border-top: none;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
[data-slot="accordion-item"]:has(+ &) {
&[data-closed] {
@@ -81,18 +86,23 @@
}
& + [data-slot="accordion-item"] {
border-top: 1px solid var(--border-weak-base);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
margin-top: 8px;
[data-slot="accordion-trigger"] {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
}
}
&[data-closed] + &[data-closed] {
[data-slot="accordion-trigger"] {
border-top: none;
}
}
&:first-child {
margin-top: 0px;
border-top: 1px solid var(--border-weak-base);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
&[data-closed] {
[data-slot="accordion-trigger"] {
@@ -104,8 +114,6 @@
&:last-child {
margin-bottom: 0px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
&[data-closed] {
[data-slot="accordion-trigger"] {
+4
View File
@@ -51,6 +51,10 @@
&:focus-visible:active {
box-shadow: none;
}
&:active {
scale: 0.99;
transition: all 150ms ease-out;
}
[data-slot="icon"] {
color: var(--icon-strong-base);
+1
View File
@@ -42,6 +42,7 @@ export function Code<T>(props: CodeProps<T>) {
"--pjs-font-features": "var(--font-family-mono--font-feature-settings)",
"--pjs-header-font-family": "var(--font-family-sans)",
"--pjs-gap-block": 0,
"--pjs-min-number-column-width": "4ch",
}}
classList={{
...(local.classList || {}),
+7 -1
View File
@@ -167,6 +167,7 @@ export function Diff<T>(props: DiffProps<T>) {
"--pjs-font-features": "var(--font-family-mono--font-feature-settings)",
"--pjs-header-font-family": "var(--font-family-sans)",
"--pjs-gap-block": 0,
"--pjs-min-number-column-width": "4ch",
}}
ref={container}
/>
@@ -231,6 +232,12 @@ registerCustomTheme("OpenCode", () => {
foreground: "var(--syntax-type)",
},
},
{
scope: ["meta.object.member"],
settings: {
foreground: "var(--syntax-primitive)",
},
},
{
scope: [
"variable.parameter.function",
@@ -238,7 +245,6 @@ registerCustomTheme("OpenCode", () => {
"meta.block",
"meta.tag.attributes",
"entity.name.constant",
"meta.object.member",
"meta.embedded.expression",
"meta.template.expression",
"string.other.begin.yaml",
+32 -31
View File
@@ -77,7 +77,7 @@
--background-weak: var(--smoke-light-3);
--background-strong: var(--smoke-light-1);
--background-stronger: #fcfcfc;
--surface-base: var(--smoke-light-alpha-2);
--surface-base: var(--smoke-light-3);
--base: var(--smoke-light-alpha-2);
--surface-base-hover: #0500000f;
--surface-base-active: var(--smoke-light-alpha-3);
@@ -267,23 +267,23 @@
--icon-diff-delete-hover: var(--ember-light-11);
--syntax-comment: var(--text-weaker);
--syntax-regexp: var(--text-base);
--syntax-string: var(--mint-light-11);
--syntax-string: #007663;
--syntax-keyword: var(--text-weak);
--syntax-primitive: var(--ember-light-11);
--syntax-primitive: #f6916b;
--syntax-operator: var(--text-weak);
--syntax-variable: var(--text-strong);
--syntax-property: var(--lilac-light-11);
--syntax-type: var(--cobalt-light-11);
--syntax-constant: var(--lilac-light-11);
--syntax-punctuation: var(--text-weak);
--syntax-object: var(--blue-light-11);
--syntax-success: var(--apple-light-10);
--syntax-warning: var(--amber-light-10);
--syntax-critical: var(--ember-light-9);
--syntax-info: var(--lilac-light-11);
--syntax-diff-add: var(--mint-light-11);
--syntax-diff-delete: var(--ember-light-11);
--syntax-unknown: red;
--syntax-property: #ed6dc8;
--syntax-type: #b2cc00;
--syntax-constant: #0092a8;
--syntax-punctuation: var(--text-weaker);
--syntax-object: var(--text-strong);
--syntax-success: var(--apple-dark-10);
--syntax-warning: var(--amber-dark-10);
--syntax-critical: var(--ember-dark-9);
--syntax-info: #0092a8;
--syntax-diff-add: var(--mint-light-alpha-8);
--syntax-diff-delete: var(--ember-light-alpha-8);
--syntax-unknown: var(--text-weak);
--markdown-heading: #d68c27;
--markdown-text: #1a1a1a;
--markdown-link: #3b7dd8;
@@ -314,10 +314,10 @@
/* OC-1-Dark */
color-scheme: dark;
--background-base: var(--smoke-dark-1);
--background-weak: #201d1d;
--background-weak: #1b1818;
--background-strong: #151313;
--background-stronger: #201c1c;
--surface-base: var(--smoke-dark-alpha-2);
--background-stronger: #191515;
--surface-base: var(--smoke-dark-3);
--base: var(--smoke-dark-alpha-2);
--surface-base-hover: #e0b7b716;
--surface-base-active: var(--smoke-dark-alpha-3);
@@ -340,7 +340,7 @@
--surface-weak: var(--smoke-dark-alpha-4);
--surface-weaker: var(--smoke-dark-alpha-5);
--surface-strong: var(--smoke-dark-alpha-7);
--surface-raised-stronger-non-alpha: var(--smoke-dark-4);
--surface-raised-stronger-non-alpha: var(--smoke-dark-3);
--surface-brand-base: var(--yuzu-light-9);
--surface-brand-hover: var(--yuzu-light-10);
--surface-interactive-base: var(--cobalt-light-3);
@@ -409,7 +409,7 @@
--text-on-brand-weak: var(--smoke-dark-alpha-9);
--text-on-brand-weaker: var(--smoke-dark-alpha-8);
--text-on-brand-strong: var(--smoke-dark-alpha-12);
--button-secondary-base: var(--smoke-dark-4);
--button-secondary-base: #231f1f;
--button-secondary-hover: #2a2727;
--border-base: var(--smoke-dark-alpha-7);
--border-hover: var(--smoke-dark-alpha-8);
@@ -417,7 +417,7 @@
--border-selected: var(--cobalt-dark-alpha-11);
--border-disabled: var(--smoke-dark-alpha-8);
--border-focus: var(--smoke-dark-alpha-9);
--border-weak-base: var(--smoke-dark-alpha-6);
--border-weak-base: var(--smoke-dark-alpha-4);
--border-strong-base: var(--smoke-dark-alpha-8);
--border-strong-hover: var(--smoke-dark-alpha-7);
--border-strong-active: var(--smoke-dark-alpha-8);
@@ -507,22 +507,23 @@
--icon-diff-delete-hover: var(--ember-dark-10);
--syntax-comment: var(--text-weaker);
--syntax-regexp: var(--text-base);
--syntax-string: var(--mint-dark-11);
--syntax-string: #00ceb9;
--syntax-keyword: var(--text-weak);
--syntax-primitive: var(--ember-dark-11);
--syntax-primitive: #ffba92;
--syntax-operator: var(--text-weak);
--syntax-variable: var(--text-strong);
--syntax-property: var(--lilac-dark-11);
--syntax-type: var(--cobalt-dark-11);
--syntax-constant: var(--lilac-dark-11);
--syntax-punctuation: var(--text-weak);
--syntax-object: var(--blue-dark-11);
--syntax-property: #ff9ae2;
--syntax-type: #ecf58c;
--syntax-constant: #93e9f6;
--syntax-punctuation: var(--text-weaker);
--syntax-object: var(--text-strong);
--syntax-success: var(--apple-dark-10);
--syntax-warning: var(--amber-dark-10);
--syntax-critical: var(--ember-dark-9);
--syntax-info: var(--lilac-dark-11);
--syntax-diff-add: var(--mint-dark-11);
--syntax-diff-delete: var(--ember-dark-11);
--syntax-info: #ff9ae2;
--syntax-diff-add: var(--mint-dark-alpha-6);
--syntax-diff-delete: var(--ember-dark-alpha-4);
--syntax-unknown: var(--text-weak);
--markdown-heading: #9d7cd8;
--markdown-text: #eeeeee;
--markdown-link: #fab283;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.55",
"version": "1.0.62",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+14 -1
View File
@@ -46,7 +46,20 @@ You can also bind a keyboard shortcut by editing your `keymap.json`:
[
{
"bindings": {
"cmd-alt-o": ["agent::NewExternalAgentThread", { "agent": "OpenCode" }]
"cmd-alt-o": [
"agent::NewExternalAgentThread",
{
"agent": {
"custom": {
"name": "OpenCode",
"command": {
"command": "opencode",
"args": ["acp"]
}
}
}
}
]
}
}
]
+5 -5
View File
@@ -16,7 +16,7 @@ OpenCode supports both **JSON** and **JSONC** (JSON with Comments) formats.
"$schema": "https://opencode.ai/config.json",
// Theme configuration
"theme": "opencode",
"model": "anthropic/claude-sonnet-4-20250514",
"model": "anthropic/claude-sonnet-4-5",
"autoupdate": true,
}
```
@@ -128,8 +128,8 @@ You can configure the providers and models you want to use in your OpenCode conf
{
"$schema": "https://opencode.ai/config.json",
"provider": {},
"model": "anthropic/claude-sonnet-4-20250514",
"small_model": "anthropic/claude-3-5-haiku-20241022"
"model": "anthropic/claude-sonnet-4-5",
"small_model": "anthropic/claude-haiku-4-5"
}
```
@@ -164,7 +164,7 @@ You can configure specialized agents for specific tasks through the `agent` opti
"agent": {
"code-reviewer": {
"description": "Reviews code for best practices and potential issues",
"model": "anthropic/claude-sonnet-4-20250514",
"model": "anthropic/claude-sonnet-4-5",
"prompt": "You are a code reviewer. Focus on security, performance, and maintainability.",
"tools": {
// Disable file modification tools for review-only agent
@@ -213,7 +213,7 @@ You can configure custom commands for repetitive tasks through the `command` opt
"template": "Run the full test suite with coverage report and show any failures.\nFocus on the failing tests and suggest fixes.",
"description": "Run tests with coverage",
"agent": "build",
"model": "anthropic/claude-3-5-sonnet-20241022",
"model": "anthropic/claude-haiku-4-5",
},
"component": {
"template": "Create a new React component named $ARGUMENTS with TypeScript support.\nInclude proper typing and basic structure.",
+23 -22
View File
@@ -41,36 +41,37 @@ You can also install it with the following commands:
- **Using Node.js**
<Tabs>
<Tabs>
<TabItem label="npm">
```bash
npm install -g opencode-ai
```
</TabItem>
<TabItem label="Bun">
<TabItem label="npm">
```bash
bun install -g opencode-ai
npm install -g opencode-ai
```
</TabItem>
</TabItem>
<TabItem label="pnpm">
```bash
pnpm install -g opencode-ai
```
<TabItem label="Bun">
```bash
bun install -g opencode-ai
```
</TabItem>
</TabItem>
<TabItem label="Yarn">
```bash
yarn global add opencode-ai
```
<TabItem label="pnpm">
```bash
pnpm install -g opencode-ai
```
</TabItem>
</Tabs>
</TabItem>
<TabItem label="Yarn">
```bash
yarn global add opencode-ai
```
</TabItem>
</Tabs>
- **Using Homebrew on macOS and Linux**
+19 -18
View File
@@ -11,24 +11,25 @@ OpenCode integrates with your Language Server Protocol (LSP) to help the LLM int
OpenCode comes with several built-in LSP servers for popular languages:
| LSP Server | Extensions | Requirements |
| ---------- | ---------------------------------------------------- | ------------------------------------------------------------ |
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` command available (auto-detects deno.json/deno.jsonc) |
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | `eslint` dependency in project |
| gopls | .go | `go` command available |
| ruby-lsp | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
| pyright | .py, .pyi | `pyright` dependency installed |
| elixir-ls | .ex, .exs | `elixir` command available |
| zls | .zig, .zon | `zig` command available |
| csharp | .cs | `.NET SDK` installed |
| vue | .vue | Auto-installs for Vue projects |
| rust | .rs | `rust-analyzer` command available |
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
| svelte | .svelte | Auto-installs for Svelte projects |
| astro | .astro | Auto-installs for Astro projects |
| jdtls | .java | `Java SDK (version 21+)` installed |
| lua-ls | .lua | Auto-installs for Lua projects |
| LSP Server | Extensions | Requirements |
| ------------- | ---------------------------------------------------- | ------------------------------------------------------------ |
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` command available (auto-detects deno.json/deno.jsonc) |
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | `eslint` dependency in project |
| gopls | .go | `go` command available |
| ruby-lsp | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
| pyright | .py, .pyi | `pyright` dependency installed |
| elixir-ls | .ex, .exs | `elixir` command available |
| zls | .zig, .zon | `zig` command available |
| csharp | .cs | `.NET SDK` installed |
| vue | .vue | Auto-installs for Vue projects |
| rust | .rs | `rust-analyzer` command available |
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
| svelte | .svelte | Auto-installs for Svelte projects |
| astro | .astro | Auto-installs for Astro projects |
| jdtls | .java | `Java SDK (version 21+)` installed |
| lua-ls | .lua | Auto-installs for Lua projects |
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) |
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
@@ -256,7 +256,7 @@ Below are examples of some common MCP servers. You can submit a PR if you want t
### Context7
Add the [Context7 MCP server](https://github.com/context-labs/mcp-server-context7) to search through docs.
Add the [Context7 MCP server](https://github.com/upstash/context7) to search through docs.
```json title="opencode.json" {4-7}
{
+46 -6
View File
@@ -417,13 +417,13 @@ To use Google Vertex AI with OpenCode:
1. Head over to the **Model Garden** in the Google Cloud Console and check the
models available in your region.
:::tip
:::note
You need to have a Google Cloud project with Vertex AI API enabled.
:::
2. Set the required environment variables:
- `GOOGLE_VERTEX_PROJECT`: Your Google Cloud project ID
- `GOOGLE_VERTEX_REGION` (optional): The region for Vertex AI (defaults to `us-east5`)
- `GOOGLE_CLOUD_PROJECT`: Your Google Cloud project ID
- `VERTEX_LOCATION` (optional): The region for Vertex AI (defaults to `global`)
- Authentication (choose one):
- `GOOGLE_APPLICATION_CREDENTIALS`: Path to your service account JSON key file
- Authenticate using gcloud CLI: `gcloud auth application-default login`
@@ -431,17 +431,21 @@ To use Google Vertex AI with OpenCode:
Set them while running opencode.
```bash
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json GOOGLE_VERTEX_PROJECT=your-project-id opencode
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json GOOGLE_CLOUD_PROJECT=your-project-id opencode
```
Or add them to your bash profile.
```bash title="~/.bash_profile"
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
export GOOGLE_VERTEX_PROJECT=your-project-id
export GOOGLE_VERTEX_REGION=us-central1
export GOOGLE_CLOUD_PROJECT=your-project-id
export VERTEX_LOCATION=global
```
:::tip
The `global` region improves availability and reduces errors at no extra cost. Use regional endpoints (e.g., `us-central1`) for data residency requirements. [Learn more](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-partner-models#regional_and_global_endpoints)
:::
3. Run the `/models` command to select the model you want.
---
@@ -779,6 +783,42 @@ OpenCode Zen is a list of tested and verified models provided by the OpenCode te
---
### OVHcloud AI Endpoints
1. Head over to the [OVHcloud panel](https://ovh.com/manager). Navigate to the `Public Cloud` section, `AI & Machine Learning` > `AI Endpoints` and in `API Keys` tab, click **Create a new API key**.
2. Run `opencode auth login` and select **OVHcloud AI Endpoints**.
```bash
$ opencode auth login
┌ Add credential
◆ Select provider
│ ● OVHcloud AI Endpoints
│ ...
```
3. Enter your OVHcloud AI Endpoints API key.
```bash
$ opencode auth login
┌ Add credential
◇ Select provider
│ OVHcloud AI Endpoints
◇ Enter your API key
│ _
```
4. Run the `/models` command to select a model like _gpt-oss-120b_.
---
### Together AI
1. Head over to the [Together AI console](https://api.together.ai), create an account, and click **Add Key**.
+4 -1
View File
@@ -49,6 +49,7 @@ if (!Script.preview) {
- Do NOT make general statements about "improvements", be very specific about what was changed.
- Do NOT include any information about code changes if they do not affect the user facing changes.
- For commits that are already well-written and descriptive, avoid rewording them. Simply capitalize the first letter, fix any misspellings, and ensure proper English grammar.
- DO NOT read any other commits than the ones listed above (THIS IS IMPORTANT TO AVOID DUPLICATING THINGS IN OUR CHANGELOG)
IMPORTANT: ONLY return a bulleted list of changes, do not include any other information. Do not include a preamble like "Based on my analysis..."
@@ -67,7 +68,9 @@ if (!Script.preview) {
notes.push(line)
}
}
console.log(notes)
console.log("---- Generated Changelog ----")
console.log(notes.join("\n"))
console.log("-----------------------------")
opencode.server.close()
}
+3 -4
View File
@@ -7,7 +7,6 @@ import { join } from "path"
const FORK_REPO = "sst/zed-extensions"
const UPSTREAM_REPO = "zed-industries/extensions"
const EXTENSION_NAME = "opencode"
const OPENCODE_REPO = "sst/opencode"
async function main() {
const version = process.argv[2]
@@ -40,8 +39,8 @@ async function main() {
process.chdir(workDir)
// Configure git identity
await $`git config user.name "github-actions[bot]"`
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`
await $`git config user.name "Dax"`
await $`git config user.email "mail@thdxr.com"`
// Sync fork with upstream
console.log(`🔄 Syncing fork with upstream...`)
@@ -108,7 +107,7 @@ async function main() {
console.log(`📬 Creating pull request...`)
const prUrl =
await $`gh pr create --repo ${UPSTREAM_REPO} --base main --head ${FORK_REPO.split("/")[0]}:${branchName} --title "Update ${EXTENSION_NAME} to v${cleanVersion}" --body "Release notes:\n\nhttps://github.com/${OPENCODE_REPO}/releases/tag/v${cleanVersion}"`.text()
await $`gh pr create --repo ${UPSTREAM_REPO} --base main --head ${FORK_REPO.split("/")[0]}:${branchName} --title "Update ${EXTENSION_NAME} to v${cleanVersion}" --body "Updating OpenCode extension to v${cleanVersion}"`.text()
console.log(`✅ Pull request created: ${prUrl}`)
console.log(`🎉 Done!`)
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.55",
"version": "1.0.62",
"publisher": "sst-dev",
"repository": {
"type": "git",
-461
View File
@@ -1,461 +0,0 @@
package com.example.theme;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.*;
import java.time.*;
import java.time.format.*;
import java.net.*;
import java.io.*;
import java.nio.file.*;
import java.sql.*;
import java.lang.annotation.*;
import java.math.BigDecimal;
import java.math.BigInteger;
// Enum definition
public enum LogLevel {
DEBUG(0, "Debug"),
INFO(1, "Info"),
WARN(2, "Warning"),
ERROR(3, "Error");
private final int level;
private final String description;
LogLevel(int level, String description) {
this.level = level;
this.description = description;
}
public int getLevel() { return level; }
public String getDescription() { return description; }
}
// Interface with generics
public interface Repository<T extends Entity> {
Optional<T> findById(Long id);
List<T> findAll();
T save(T entity);
void delete(Long id);
Stream<T> stream();
@FunctionalInterface
interface Predicate<T> {
boolean test(T t);
}
}
// Abstract class
public abstract class AbstractService<T extends Entity> implements Repository<T> {
protected final Map<Long, T> cache = new ConcurrentHashMap<>();
protected volatile boolean initialized = false;
@Override
public Optional<T> findById(Long id) {
return Optional.ofNullable(cache.get(id));
}
@Override
public List<T> findAll() {
return new ArrayList<>(cache.values());
}
@Override
public Stream<T> stream() {
return cache.values().stream();
}
protected abstract void validate(T entity) throws ValidationException;
}
// Annotation definition
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Service {
String value() default "";
boolean transactional() default true;
Class<?>[] exceptions() = {};
}
// Record class (Java 14+)
public record User(
Long id,
String username,
String email,
@Deprecated String fullName,
LocalDateTime createdAt,
boolean active
) implements Entity {
public User {
Objects.requireNonNull(username, "Username cannot be null");
Objects.requireNonNull(email, "Email cannot be null");
Objects.requireNonNull(createdAt, "Created date cannot be null");
}
public static User of(String username, String email) {
return new User(null, username, email, null, LocalDateTime.now(), true);
}
public User withId(Long id) {
return new User(id, username, email, fullName, createdAt, active);
}
}
// Exception classes
public class ValidationException extends RuntimeException {
private final List<String> errors;
public ValidationException(String message) {
super(message);
this.errors = List.of(message);
}
public ValidationException(List<String> errors) {
super(String.join(", ", errors));
this.errors = Collections.unmodifiableList(errors);
}
public List<String> getErrors() { return errors; }
}
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String resource, Long id) {
super(String.format("%s with id %d not found", resource, id));
}
}
// Service implementation
@Service(value = "userService", transactional = true)
public class UserService extends AbstractService<User> {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
private static final int MAX_RETRY_ATTEMPTS = 3;
private static final Duration TIMEOUT = Duration.ofSeconds(30);
private final EmailService emailService;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Inject
public UserService(EmailService emailService,
UserRepository userRepository,
PasswordEncoder passwordEncoder) {
this.emailService = emailService;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
protected void validate(User user) throws ValidationException {
List<String> errors = new ArrayList<>();
if (user.username() == null || user.username().trim().isEmpty()) {
errors.add("Username is required");
} else if (!user.username().matches("^[a-zA-Z0-9_]{3,20}$")) {
errors.add("Username must be 3-20 characters, alphanumeric and underscore only");
}
if (user.email() == null || !user.email().matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
errors.add("Valid email is required");
}
if (!errors.isEmpty()) {
throw new ValidationException(errors);
}
}
@Transactional
public User createUser(CreateUserRequest request) throws ValidationException {
logger.info("Creating new user: {}", request.username());
// Check if user already exists
if (userRepository.findByUsername(request.username()).isPresent()) {
throw new ValidationException("Username already exists");
}
if (userRepository.findByEmail(request.email()).isPresent()) {
throw new ValidationException("Email already exists");
}
// Create new user
User user = User.of(request.username(), request.email())
.withId(generateId());
validate(user);
try {
User savedUser = userRepository.save(user);
cache.put(savedUser.id(), savedUser);
// Send welcome email asynchronously
CompletableFuture.runAsync(() ->
emailService.sendWelcomeEmail(savedUser)
).exceptionally(throwable -> {
logger.error("Failed to send welcome email to user {}", savedUser.id(), throwable);
return null;
});
logger.info("Successfully created user with ID: {}", savedUser.id());
return savedUser;
} catch (DataAccessException e) {
logger.error("Database error while creating user", e);
throw new ServiceException("Failed to create user", e);
}
}
public Optional<User> findByUsername(String username) {
return cache.values().stream()
.filter(user -> user.username().equals(username))
.findFirst();
}
public List<User> findActiveUsers() {
return cache.values().stream()
.filter(User::active)
.sorted(Comparator.comparing(User::createdAt).reversed())
.collect(Collectors.toList());
}
@Retry(maxAttempts = MAX_RETRY_ATTEMPTS, backoff = @Backoff(delay = 1000))
public User updateUser(Long id, UpdateUserRequest request) {
User existingUser = findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
User updatedUser = new User(
id,
request.username() != null ? request.username() : existingUser.username(),
request.email() != null ? request.email() : existingUser.email(),
existingUser.fullName(),
existingUser.createdAt(),
request.active() != null ? request.active() : existingUser.active()
);
validate(updatedUser);
try {
User savedUser = userRepository.save(updatedUser);
cache.put(id, savedUser);
return savedUser;
} catch (DataAccessException e) {
logger.error("Failed to update user with ID: {}", id, e);
throw new ServiceException("Failed to update user", e);
}
}
@Async
public CompletableFuture<Void> deleteUser(Long id) {
return CompletableFuture.runAsync(() -> {
try {
userRepository.deleteById(id);
cache.remove(id);
logger.info("Successfully deleted user with ID: {}", id);
} catch (DataAccessException e) {
logger.error("Failed to delete user with ID: {}", id, e);
throw new ServiceException("Failed to delete user", e);
}
});
}
private Long generateId() {
return System.currentTimeMillis() + (long)(Math.random() * 1000);
}
}
// Utility class
public final class DateUtils {
private DateUtils() {
// Utility class - prevent instantiation
}
private static final DateTimeFormatter ISO_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
public static String formatIsoDateTime(LocalDateTime dateTime) {
return dateTime.atZone(ZoneId.systemDefault()).format(ISO_FORMATTER);
}
public static LocalDateTime parseIsoDateTime(String isoString) {
return LocalDateTime.parse(isoString, ISO_FORMATTER);
}
public static boolean isWithinLastDays(LocalDateTime dateTime, int days) {
return dateTime.isAfter(LocalDateTime.now().minusDays(days));
}
}
// Builder pattern
public class UserQueryBuilder {
private String username;
private String email;
private Boolean active;
private LocalDateTime createdAfter;
private LocalDateTime createdBefore;
private SortOrder sortOrder = SortOrder.ASC;
private String sortBy = "createdAt";
private int limit = 100;
private int offset = 0;
public UserQueryBuilder withUsername(String username) {
this.username = username;
return this;
}
public UserQueryBuilder withEmail(String email) {
this.email = email;
return this;
}
public UserQueryBuilder activeOnly(boolean active) {
this.active = active;
return this;
}
public UserQueryBuilder createdAfter(LocalDateTime date) {
this.createdAfter = date;
return this;
}
public UserQueryBuilder createdBefore(LocalDateTime date) {
this.createdBefore = date;
return this;
}
public UserQueryBuilder sortBy(String field, SortOrder order) {
this.sortBy = field;
this.sortOrder = order;
return this;
}
public UserQueryBuilder limit(int limit) {
this.limit = Math.max(1, Math.min(limit, 1000));
return this;
}
public UserQueryBuilder offset(int offset) {
this.offset = Math.max(0, offset);
return this;
}
public UserQuery build() {
return new UserQuery(username, email, active, createdAfter, createdBefore,
sortBy, sortOrder, limit, offset);
}
}
// Lambda expressions and streams
public class StreamProcessor {
private static final Map<String, Function<String, Object>> TYPE_CONVERTERS = Map.of(
"string", s -> s,
"int", Integer::parseInt,
"double", Double::parseDouble,
"boolean", Boolean::parseBoolean,
"bigdecimal", BigDecimal::new,
"bigint", BigInteger::new
);
public Map<String, Object> processConfig(Properties properties) {
return properties.entrySet().stream()
.filter(entry -> entry.getKey() instanceof String)
.filter(entry -> entry.getValue() != null)
.collect(Collectors.toMap(
entry -> (String) entry.getKey(),
entry -> convertValue((String) entry.getKey(), (String) entry.getValue())
));
}
private Object convertValue(String key, String value) {
String type = determineType(key, value);
return TYPE_CONVERTERS.getOrDefault(type, Function.identity()).apply(value);
}
private String determineType(String key, String value) {
if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
return "boolean";
} else if (value.matches("-?\\d+")) {
return "int";
} else if (value.matches("-?\\d*\\.\\d+")) {
return "double";
} else if (key.toLowerCase().contains("amount") || key.toLowerCase().contains("price")) {
return "bigdecimal";
}
return "string";
}
public List<String> validateEmails(List<String> emails) {
return emails.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(email -> !email.isEmpty())
.filter(email -> email.matches("^[A-Za-z0-9+_.-]+@(.+)$"))
.distinct()
.collect(Collectors.toList());
}
public CompletableFuture<List<User>> processUsersAsync(List<User> users) {
return CompletableFuture.supplyAsync(() ->
users.parallelStream()
.filter(User::active)
.filter(user -> user.createdAt().isAfter(LocalDateTime.now().minusYears(1)))
.sorted(Comparator.comparing(User::username))
.collect(Collectors.toList())
);
}
}
// Main class for testing
public class Main {
private static final Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
try {
// Initialize application context
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// Get service bean
UserService userService = context.getBean(UserService.class);
// Create test users
List<User> users = Arrays.asList(
User.of("john_doe", "john@example.com"),
User.of("jane_smith", "jane@example.com"),
User.of("bob_wilson", "bob@example.com")
);
// Process users
List<CompletableFuture<User>> futures = users.stream()
.map(user -> {
try {
return CompletableFuture.completedFuture(userService.createUser(
new CreateUserRequest(user.username(), user.email())
));
} catch (ValidationException e) {
logger.error("Failed to create user: {}", user.username(), e);
return CompletableFuture.<User>failedFuture(e);
}
})
.collect(Collectors.toList());
// Wait for all to complete
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenRun(() -> {
logger.info("All users created successfully");
System.out.println("Application started successfully!");
})
.exceptionally(throwable -> {
logger.error("Failed to initialize users", throwable);
System.err.println("Application startup failed!");
return null;
});
} catch (Exception e) {
logger.error("Application startup failed", e);
System.exit(1);
}
}
}
-669
View File
@@ -1,669 +0,0 @@
# TextMate Grammar Token Examples
This file contains examples of every major TextMate token style for theme testing.
## Comments
<!-- HTML comment -->
// Single line comment
/_ Multi-line comment _/
# Shell comment
/_ JSDoc comment with @param and @return _/
## Strings
"Double quoted string"
'Single quoted string'
`Backtick string`
"String with \"escaped\" quotes"
'String with \'escaped\' quotes'
`String with \`escaped\` backticks`
## Template Literals
`Simple template literal`
`Template with ${variable} interpolation`
`Template with ${function.call()} expression`
Multi-line template with ${nested.interpolation}
## Numbers
42
-17
3.14159
-0.001
1e10
-2.5e-8
0xFF
0o755
0b1010
## Keywords
if else elif for while do switch case default
function class extends implements import export
return break continue throw try catch finally
var let const static async await yield
new this super null undefined true false
## Storage Types
int float double string boolean char void
static final abstract private public protected
readonly volatile transient synchronized
## Constants
MAX_VALUE
DEFAULT_TIMEOUT
API_ENDPOINT
PI
E
## Variables
variableName
\_privateVariable
$specialVariable
camelCase
snake_case
PascalCase
kebab-case
## Functions
functionName()
method.call()
object.property()
array[index]
arrowFunction => expression
## Operators
- - - / % ++ --
== === != !== > < >= <=
&& || ! & | ^ ~ << >> >>>
= += -= \*= /= %= &= |= ^= <<= >>= >>>=
## Punctuation
, ; : . ... ( ) [ ] { } < > / \\
# @ $ % ^ & \* - \_ + = | ~ ` ?
## Entities
ClassName
InterfaceName
EnumName
TypeName
MethodName
PropertyName
## Tags
<div>
<span>
<p>
<a href="link">
<img src="image.jpg" alt="description" />
## Attributes
class="container"
id="main"
data-value="123"
disabled
required
readonly
## CSS Selectors & Properties
.container
#header
.button:hover
input[type="text"]
::before
::after
color: #ffffff;
background: linear-gradient(45deg, #ff0000, #00ff00);
font-size: 16px;
margin: 0 auto;
padding: 10px 20px;
## Regular Expressions
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
/\d{3}-\d{3}-\d{4}/g
/(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})/
## URLs & Paths
https://example.com/path/to/resource
file:///Users/username/project
./relative/path
../parent/directory
/home/user/documents
## JSON
{
"name": "example",
"version": "1.0.0",
"dependencies": {
"react": "^18.0.0",
"typescript": "^4.9.0"
},
"scripts": {
"start": "node index.js",
"test": "jest"
}
}
## XML/HTML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div class="content">
<h1>Title</h1>
<p>Paragraph text</p>
</div>
</body>
</html>
## SQL
SELECT u.id, u.name, u.email, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.active = true
AND o.created_at >= '2023-01-01'
GROUP BY u.id, u.name, u.email
HAVING COUNT(o.id) > 5
ORDER BY order_count DESC
LIMIT 10;
## GraphQL
query GetUserProfile($userId: ID!, $includePosts: Boolean!) {
user(id: $userId) {
id
name
email
avatar
createdAt
posts @include(if: $includePosts) {
id
title
content
publishedAt
comments(first: 10) {
edges {
node {
id
author
content
createdAt
}
}
}
}
}
}
## Shell/Bash
#!/bin/bash
# Variables
PROJECT*DIR="/home/user/projects"
BACKUP_DIR="$PROJECT_DIR/backups"
TIMESTAMP=$(date +"%Y%m%d*%H%M%S")
# Functions
create*backup() {
local source_dir=$1
local backup_file="$BACKUP_DIR/backup*$TIMESTAMP.tar.gz"
echo "Creating backup of $source_dir..."
tar -czf "$backup_file" "$source_dir"
echo "Backup created: $backup_file"
}
# Conditional logic
if [ -d "$PROJECT_DIR" ]; then
create_backup "$PROJECT_DIR"
else
echo "Project directory not found: $PROJECT_DIR"
exit 1
fi
## Python
import os
import sys
from typing import List, Dict, Optional
import requests
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
email: Optional[str] = None
active: bool = True
def __post_init__(self):
if not self.name.strip():
raise ValueError("Name cannot be empty")
class UserService:
def **init**(self, api_url: str):
self.api_url = api_url
self.session = requests.Session()
async def get_user(self, user_id: int) -> Optional[User]:
"""Fetch user data from API."""
try:
response = await self.session.get(f"{self.api_url}/users/{user_id}")
response.raise_for_status()
data = response.json()
return User(**data)
except requests.RequestException as e:
print(f"Error fetching user {user_id}: {e}")
return None
## Rust
use std::collections::HashMap;
use std::fs::File;
use std::io::{self, Read};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: u64,
pub name: String,
pub email: Option<String>, #[serde(default)]
pub active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
}
impl User {
pub fn new(id: u64, name: String, email: Option<String>) -> Self {
Self {
id,
name,
email,
active: true,
created_at: chrono::Utc::now(),
}
}
pub fn display_name(&self) -> String {
match &self.email {
Some(email) => format!("{} <{}>", self.name, email),
None => self.name.clone(),
}
}
}
pub struct UserService {
api_url: String,
client: reqwest::Client,
}
impl UserService {
pub fn new(api_url: String) -> Self {
Self {
api_url,
client: reqwest::Client::new(),
}
}
pub async fn get_user(&self, user_id: u64) -> Result<User, Box<dyn std::error::Error>> {
let url = format!("{}/users/{}", self.api_url, user_id);
let response = self.client.get(&url).send().await?;
let user: User = response.json().await?;
Ok(user)
}
}
## Go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/mux"
)
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email \*string `json:"email,omitempty"`
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
}
type UserService struct {
re UserRepository
}
func NewUserService(repo UserRepository) \*UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get user %d: %w", id, err)
}
return user, nil
}
func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) (\*User, error) {
user := &User{
Name: req.Name,
Email: req.Email,
Active: true,
CreatedAt: time.Now(),
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return user, nil
}
## YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: production
labels:
app: web-app
version: v1.2.3
spec:
replicas: 3
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
tier: frontend
spec:
containers: - name: web-app
image: nginx:1.21-alpine
ports: - containerPort: 80
protocol: TCP
env: - name: NODE_ENV
value: "production" - name: API_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: api-url
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 80
initialDelaySeconds: 5
periodSeconds: 5
## TOML
[project]
name = "example-app"
version = "1.0.0"
description = "An example application"
authors = ["John Doe <john@example.com>"]
license = "MIT"
readme = "README.md"
homepage = "https://example.com"
repository = "https://github.com/johndoe/example-app"
keywords = ["web", "api", "rust"]
categories = ["web-programming"]
edition = "2021"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
chrono = { version = "0.4", features = ["serde"] }
log = "0.4"
env_logger = "0.10"
[dev-dependencies]
tokio-test = "0.4"
mockito = "1.0"
[[bin]]
name = "server"
path = "src/main.rs"
[[bin]]
name = "client"
path = "src/client.rs"
## Dockerfile
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml\* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD ["node", "server.js"]
## Makefile
.PHONY: help build test clean install dev lint format
# Default target
.DEFAULT_GOAL := help
# Variables
APP_NAME := myapp
VERSION := $(shell git describe --tags --always --dirty)
BUILD_DIR := ./build
DIST_DIR := ./dist
GO_FILES := $(shell find . -name '\*.go' -type f)
help: ## Show this help message
@echo "Available targets:"
@grep -E '^[a-zA-Z_-]+:._?## ._$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.\*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
install: ## Install dependencies
go mod download
npm install
build: ## Build the application
@echo "Building $(APP_NAME) version $(VERSION)..."
mkdir -p $(BUILD_DIR)
go build -ldflags "-X main.version=$(VERSION)" -o $(BUILD_DIR)/$(APP_NAME) ./cmd/main.go
test: ## Run tests
go test -v ./...
npm test
lint: ## Run linters
golangci-lint run
npx eslint .
format: ## Format code
go fmt ./...
npx prettier --write .
dev: ## Run in development mode
go run ./cmd/main.go --dev
clean: ## Clean build artifacts
rm -rf $(BUILD_DIR)
rm -rf $(DIST_DIR)
go clean -cache
docker-build: ## Build Docker image
docker build -t $(APP_NAME):$(VERSION) .
docker tag $(APP_NAME):$(VERSION) $(APP_NAME):latest
docker-run: ## Run Docker container
docker run -p 8080:8080 $(APP_NAME):latest
release: ## Create a new release
@echo "Creating release $(VERSION)"
git tag -a $(VERSION) -m "Release $(VERSION)"
git push origin $(VERSION)
goreleaser release --rm-dist
## Git Diff
diff --git a/src/components/UserProfile.tsx b/src/components/UserProfile.tsx
index 1234567..abcdefg 100644
--- a/src/components/UserProfile.tsx
+++ b/src/components/UserProfile.tsx
@@ -10,7 +10,7 @@ interface User {
id: number
name: string
email?: string
- createdAt: Date
* readonly createdAt: Date
active: boolean
}
@@ -25,8 +25,12 @@ const UserProfile: FC<{ user: User }> = ({ user }) => {
const [isEditing, setIsEditing] = useState(false)
const [formData, setFormData] = useState(user)
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- // Handle form submission
- }
- return (
* <div className="user-card">
- <div className={`user-card ${user.active ? 'active' : 'inactive'}`}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
-347
View File
@@ -1,347 +0,0 @@
import React, { useState, useEffect, useCallback, useMemo } from "react"
import { ComponentType, FC, ReactNode, JSX } from "react"
// Interface definitions
interface User {
id: number
name: string
email?: string
readonly createdAt: Date
active: boolean
}
type Theme = "light" | "dark" | "auto"
type Status = "pending" | "loading" | "success" | "error"
// Generic function with constraints
function createRepository<T extends { id: number }>(items: T[]): Repository<T> {
return new Repository(items)
}
// Class definition
class Repository<T> {
private items: T[]
protected cache: Map<string, T>
constructor(items: T[] = []) {
this.items = items
this.cache = new Map()
}
public find(id: number): T | undefined {
const x = undefined
type x = { foo: undefined }
return this.items.find((item) => item.id === id)
}
public async findAll(): Promise<T[]> {
return this.items
}
get count(): number {
return this.items.length
}
}
// Enum definition
enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}
// Constants
const API_URL = "https://api.example.com"
const MAX_RETRIES = 3
const DEFAULT_TIMEOUT = 5000
// Regular expressions
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
const PHONE_REGEX = /^\+?1?-?\.?\s?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})$/
// Template literals
const greeting = `Hello, ${user.name}!`
const sql = `
SELECT * FROM users
WHERE active = true
AND created_at > '${new Date().toISOString()}'
`
// String source examples (CSS-in-JS, GraphQL, etc.)
const styledComponent = css`
.container {
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
}
.title {
font-size: 1.5rem;
font-weight: bold;
color: white;
margin-bottom: 1rem;
}
}
`
const graphqlQuery = `
query GetUserProfile($userId: ID!) {
user(id: $userId) {
id
name
email
avatar
createdAt
posts {
id
title
content
publishedAt
comments {
id
author
content
createdAt
}
}
}
}
`
const htmlTemplate = `
<div class="user-card">
<img src="${user.avatar}" alt="${user.name}" class="avatar" />
<div class="user-info">
<h3>${user.name}</h3>
<p>${user.email}</p>
<span class="status ${user.active ? "active" : "inactive"}">
${user.active ? "Active" : "Inactive"}
</span>
</div>
<div class="actions">
<button onclick="editUser(${user.id})">Edit</button>
<button onclick="deleteUser(${user.id})" class="danger">Delete</button>
</div>
</div>
`
// Arrow functions
const debounce = <T extends (...args: any[]) => any>(func: T, wait: number): ((...args: Parameters<T>) => void) => {
let timeout: NodeJS.Timeout
return (...args: Parameters<T>) => {
clearTimeout(timeout)
timeout = setTimeout(() => func(...args), wait)
}
}
// Async function
async function fetchUserData(userId: number): Promise<User> {
try {
const response = await fetch(`${API_URL}/users/${userId}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
} catch (error) {
console.error("Failed to fetch user data:", error)
throw error
}
}
// React component with various patterns
const ThemeProvider: FC<{ children: ReactNode; theme?: Theme }> = ({ children, theme = "auto" }) => {
const [currentTheme, setCurrentTheme] = useState<Theme>(theme)
const [status, setStatus] = useState<Status>("pending")
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
const handleChange = (e: MediaQueryListEvent) => {
setCurrentTheme(e.matches ? "dark" : "light")
}
mediaQuery.addEventListener("change", handleChange)
return () => mediaQuery.removeEventListener("change", handleChange)
}, [])
const contextValue = useMemo(
() => ({
theme: currentTheme,
setTheme: setCurrentTheme,
toggleTheme: () => setCurrentTheme((prev) => (prev === "light" ? "dark" : "light")),
}),
[currentTheme],
)
return (
<ThemeContext.Provider value={contextValue}>
<div className={`theme-${currentTheme}`} data-theme={currentTheme}>
{children}
</div>
</ThemeContext.Provider>
)
}
// Higher-order component
function withLogging<P extends object>(Component: ComponentType<P>): ComponentType<P> {
return function LoggedComponent(props: P) {
console.log("Rendering component:", Component.name)
return <Component {...props} />
}
}
// Custom hook
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
const setValue = useCallback(
(value: T) => {
try {
setStoredValue(value)
window.localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error)
}
},
[key],
)
return [storedValue, setValue]
}
// JSX component with various elements
const UserProfile: FC<{ user: User; onUpdate?: (user: User) => void }> = ({ user, onUpdate }) => {
const [isEditing, setIsEditing] = useState(false)
const [formData, setFormData] = useState(user)
const [errors, setErrors] = useState<Record<string, string>>({})
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Validation
if (!formData.name.trim()) {
setErrors({ name: "Name is required" })
return
}
if (formData.email && !EMAIL_REGEX.test(formData.email)) {
setErrors({ email: "Invalid email format" })
return
}
try {
setStatus("loading")
await onUpdate?.(formData)
setStatus("success")
setIsEditing(false)
} catch (error) {
setStatus("error")
setErrors({ submit: "Failed to update profile" })
}
}
return (
<div className="user-profile">
<div className="user-header">
<h2>{user.name}</h2>
<span className={`status status-${user.active ? "active" : "inactive"}`}>
{user.active ? "Active" : "Inactive"}
</span>
</div>
{isEditing ? (
<form onSubmit={handleSubmit} className="user-form">
<div className="form-group">
<label htmlFor="name">Name:</label>
<input
id="name"
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className={errors.name ? "error" : ""}
placeholder="Enter your name"
required
/>
{errors.name && <span className="error-message">{errors.name}</span>}
</div>
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
value={formData.email || ""}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className={errors.email ? "error" : ""}
placeholder="user@example.com"
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className="form-actions">
<button type="submit" disabled={status === "loading"}>
{status === "loading" ? "Saving..." : "Save"}
</button>
<button type="button" onClick={() => setIsEditing(false)} className="secondary">
Cancel
</button>
</div>
{errors.submit && <div className="alert alert-error">{errors.submit}</div>}
</form>
) : (
<div className="user-details">
<p>
<strong>ID:</strong> {user.id}
</p>
<p>
<strong>Email:</strong> {user.email || "Not provided"}
</p>
<p>
<strong>Member since:</strong> {user.createdAt.toLocaleDateString()}
</p>
<button onClick={() => setIsEditing(true)} className="edit-button">
Edit Profile
</button>
</div>
)}
</div>
)
}
// Export statements
export {
User,
Theme,
Status,
LogLevel,
Repository,
ThemeProvider,
UserProfile,
fetchUserData,
useLocalStorage,
withLogging,
debounce,
}
export default ThemeProvider
// Type exports
export type { User as UserType, ComponentType as ReactComponentType }