mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 08:46:15 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 679a587c9d |
@@ -88,6 +88,7 @@
|
||||
"@tanstack/solid-query": "5.91.4",
|
||||
"@tanstack/solid-virtual": "catalog:",
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1",
|
||||
@@ -617,9 +618,9 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opencode-ai/theme": "workspace:*",
|
||||
"@opentui/core": ">=0.4.5",
|
||||
"@opentui/keymap": ">=0.4.5",
|
||||
"@opentui/solid": ">=0.4.5",
|
||||
"@opentui/core": "0.0.0-20260803-1395a054",
|
||||
"@opentui/keymap": "0.0.0-20260803-1395a054",
|
||||
"@opentui/solid": "0.0.0-20260803-1395a054",
|
||||
"solid-js": ">=1.9.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
@@ -910,8 +911,6 @@
|
||||
"@opentui/keymap": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"clipboardy": "4.0.0",
|
||||
"diff": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"get-east-asian-width": "catalog:",
|
||||
@@ -1097,9 +1096,9 @@
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/keymap": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@opentui/core": "0.0.0-20260803-1395a054",
|
||||
"@opentui/keymap": "0.0.0-20260803-1395a054",
|
||||
"@opentui/solid": "0.0.0-20260803-1395a054",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -1117,9 +1116,9 @@
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opentui/core": "0.4.5",
|
||||
"@opentui/keymap": "0.4.5",
|
||||
"@opentui/solid": "0.4.5",
|
||||
"@opentui/core": "0.0.0-20260803-1395a054",
|
||||
"@opentui/keymap": "0.0.0-20260803-1395a054",
|
||||
"@opentui/solid": "0.0.0-20260803-1395a054",
|
||||
"@pierre/diffs": "1.2.10",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@sentry/solid": "10.36.0",
|
||||
@@ -2158,27 +2157,27 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="],
|
||||
"@opentui/core": ["@opentui/core@0.0.0-20260803-1395a054", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.0.0-20260803-1395a054", "@opentui/core-darwin-x64": "0.0.0-20260803-1395a054", "@opentui/core-linux-arm64": "0.0.0-20260803-1395a054", "@opentui/core-linux-arm64-musl": "0.0.0-20260803-1395a054", "@opentui/core-linux-x64": "0.0.0-20260803-1395a054", "@opentui/core-linux-x64-musl": "0.0.0-20260803-1395a054", "@opentui/core-win32-arm64": "0.0.0-20260803-1395a054", "@opentui/core-win32-x64": "0.0.0-20260803-1395a054" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-FSx082sVlyiwbt7sSpFE3CxQWY240cGuqwFfiFg6UHniLqZcbFkLerwFfOOJgVzwE9Ss1+3QK+xUZ2faxdq3gw=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8KUG0oRidnR+oW1RSZJ72/PhZLl+qRRMk5U/mieF4c0SJ5V3tYACpBZAKzQfHNd1f7QzD8FHZct1lPpQgtmkWg=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20260803-1395a054", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gH9K1DBtdj2LV9Zs80/5kXtJWNorPXKMUoushvo3gf0oRhcukZRtyVssbp/R+1GEJENwBv48UkXB3F/lm8Ijlw=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-R2bocsg55gwjOqCp/MWFgFYzRmsduKegB6nzgFAPCvAD/L5Jf30xpWJWFlSg3x8vxe1L9WJ84dfqa4M7mZZ3wA=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20260803-1395a054", "", { "os": "darwin", "cpu": "x64" }, "sha512-5iNqfkyxhvh01EkY/D7PAt7kElWo9eisITfxAA8pc2/pfwIPRl81VeRCDEP7uKMwUx2E+HwZP1x+bs4PG4GLJw=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-R4MZ25a4CzOAGVjW9aj1hUfzQGVfCJwrwBDbNs2SXaIvzcZqkxCVtU4FoQ5LsaD0j/BdNQVg2CIfFkFsm1fDuQ=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20260803-1395a054", "", { "os": "linux", "cpu": "arm64" }, "sha512-san/6drDO6FNISL6GL3cDxMctls0s8pcyBvDMY1TrAHBT4ZdMXnxS83KGfA4DmXKUpRZcchf788nvJ48e8Zclw=="],
|
||||
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ieqdyKI6EIYPalYAETB2wsdP83hr5Ifi+dFnBFUmdEEFHsoKwBmn2S7bsTOYlX7Bg03F4/YPIg+IvRpeC+cUJw=="],
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.0.0-20260803-1395a054", "", { "os": "linux", "cpu": "arm64" }, "sha512-TpOWMpBmf1BdqG1GDyj1TCiJmYenq0r0BsBDlkixBpmnTXeGT4ywJAGUYOUEJB+qu4vdijJ8kuO5ataKz3giGw=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-SNyuQoxMKI1vuJhgxSSW96adWM6LqFl2SoS3GM4tGeneGOanVVG2Y06PvlytXvF4cKik97t0rqkVMRetmOs93w=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20260803-1395a054", "", { "os": "linux", "cpu": "x64" }, "sha512-8Y8oPI1LZmVezIUu0lGwPG96FImBtLRfE/+9vMsx0njTENTVJBdALD9+L4SauE8+PRiGblKR4IUMVTg5kn3Mgw=="],
|
||||
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mKVKcIcPiSVVZZsdPSBoWwoa2/TCeQAaMDeHF7PFw2kt5bTXZPP7xxWfRQLCNIcA1eaGl59UuwUWHDR2Ve548Q=="],
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.0.0-20260803-1395a054", "", { "os": "linux", "cpu": "x64" }, "sha512-UythgF6YIrsI/kRV2i21NyAJ0sTt07d1K/+uZbfAq2vMgtUjcpIYe2qKqNNel826ak40J6FB1cccFyhl//moVw=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-GHTTsqeR45q2Iek9Rb7ty+x/hAKn2jZ1ujlCgPR8LBKyF7h0E1dNFryoZ7ehMc3kJndP1sKn836IemKFqxuDdQ=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20260803-1395a054", "", { "os": "win32", "cpu": "arm64" }, "sha512-hFg41FMidMn4MqCahxwvbpi3aLF3rk6LthSS08XRzrGLFbTtCmB3uxkCsUQHVSWIzGu28Qq845YzYDu3oBoe9A=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-Y8T/yXCDGagRGiQrtmuB6AhRcPucKFs/Dre3v8kJwNYqDccI4FzUPKclZ7djfmRZNjl7JUqPhZZP/PwDpQocMg=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20260803-1395a054", "", { "os": "win32", "cpu": "x64" }, "sha512-ZxJW1X2cIohwPsVXzbG7/xX+KaRMcK46hKNMocx5XAuW6BMKL1Cj3X6BdPByAC1pDx0UQJtvBqNZqTdubFkfaA=="],
|
||||
|
||||
"@opentui/keymap": ["@opentui/keymap@0.4.5", "", { "dependencies": { "@opentui/core": "0.4.5" }, "peerDependencies": { "@opentui/react": "0.4.5", "@opentui/solid": "0.4.5", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-S1wzKHhF70zT6bH+VBFY+lSeTImLcIFW28JNQiME8MoPcy6KGPs7rKFSHrb/U7P8rsTJeRfW5A4d1Cy6PKodDg=="],
|
||||
"@opentui/keymap": ["@opentui/keymap@0.0.0-20260803-1395a054", "", { "dependencies": { "@opentui/core": "0.0.0-20260803-1395a054" }, "peerDependencies": { "@opentui/react": "0.0.0-20260803-1395a054", "@opentui/solid": "0.0.0-20260803-1395a054", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-FW480MzEwRYLA3y7XfMp2+xhrAa3uiLhpQT1uJT2k8M82eAJYAjonId1DpWhw0KLXONqeg27UMCPpAy+npvKEA=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.4.5", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.5", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-B0RSkXnrtPVfEJOX+Hj+axjLJ3lzbG1BZw5I7Pvb9OPp48Vzg2cW2a3cSa86/q48ndLt647i/XwFPIw/jqnI5g=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.0.0-20260803-1395a054", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20260803-1395a054", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-K4w7+6nyJg4lrs8psdeHDcTyYNTcLgQdSATBf5R76vX7DIK1gQx+WUHsRNDm+Q4XeIjkngGTK/GsFLviUA9MYw=="],
|
||||
|
||||
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
|
||||
|
||||
@@ -3526,7 +3525,7 @@
|
||||
|
||||
"builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="],
|
||||
|
||||
"bun-ffi-structs": ["bun-ffi-structs@0.2.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="],
|
||||
"bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="],
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||
|
||||
@@ -3606,8 +3605,6 @@
|
||||
|
||||
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
|
||||
|
||||
"clipboardy": ["clipboardy@4.0.0", "", { "dependencies": { "execa": "^8.0.1", "is-wsl": "^3.1.0", "is64bit": "^2.0.0" } }, "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
|
||||
@@ -4024,7 +4021,7 @@
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
|
||||
|
||||
"execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
|
||||
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
||||
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
|
||||
|
||||
@@ -4306,7 +4303,7 @@
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
|
||||
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
|
||||
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
|
||||
|
||||
@@ -4450,8 +4447,6 @@
|
||||
|
||||
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||
|
||||
"is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="],
|
||||
|
||||
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
|
||||
@@ -4790,7 +4785,7 @@
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||
|
||||
@@ -4910,7 +4905,7 @@
|
||||
|
||||
"npm-registry-fetch": ["npm-registry-fetch@19.1.1", "", { "dependencies": { "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", "npm-package-arg": "^13.0.0", "proc-log": "^6.0.0" } }, "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="],
|
||||
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
@@ -4936,7 +4931,7 @@
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
|
||||
|
||||
@@ -5536,7 +5531,7 @@
|
||||
|
||||
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
|
||||
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
|
||||
|
||||
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
||||
|
||||
@@ -5568,8 +5563,6 @@
|
||||
|
||||
"svgo": ["svgo@4.0.2", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng=="],
|
||||
|
||||
"system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="],
|
||||
|
||||
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||
@@ -6510,8 +6503,6 @@
|
||||
|
||||
"@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||
|
||||
"@vercel/cli-exec/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
||||
"@vercel/functions/@vercel/oidc": ["@vercel/oidc@3.8.1", "", { "dependencies": { "@vercel/cli-config": "0.2.1", "@vercel/cli-exec": "1.0.0", "jose": "^5.9.6" } }, "sha512-ufdalm2MWOYksyj8KVpWjoOFPJO6zoYpuyvIggIQ2bB0CFCjTCiTkGXHqAKwG77GVRjOaN3/8S5ITlZpXWmqOw=="],
|
||||
|
||||
"@vercel/nft/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
||||
@@ -6694,9 +6685,11 @@
|
||||
|
||||
"estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
|
||||
"execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
|
||||
"execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
@@ -6770,8 +6763,6 @@
|
||||
|
||||
"node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
|
||||
|
||||
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
@@ -7546,20 +7537,6 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@vercel/cli-exec/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"@vercel/cli-exec/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
|
||||
"@vercel/cli-exec/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"@vercel/cli-exec/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||
|
||||
"@vercel/cli-exec/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"@vercel/cli-exec/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"@vercel/cli-exec/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
|
||||
|
||||
"@vercel/functions/@vercel/oidc/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
||||
|
||||
"@vercel/routing-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
@@ -8614,8 +8591,6 @@
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||
|
||||
"@vercel/cli-exec/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||
|
||||
+6
-6
@@ -46,9 +46,9 @@
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/standard-validator": "0.2.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.4.5",
|
||||
"@opentui/keymap": "0.4.5",
|
||||
"@opentui/solid": "0.4.5",
|
||||
"@opentui/core": "0.0.0-20260803-1395a054",
|
||||
"@opentui/keymap": "0.0.0-20260803-1395a054",
|
||||
"@opentui/solid": "0.0.0-20260803-1395a054",
|
||||
"@tanstack/solid-virtual": "3.13.32",
|
||||
"@shikijs/stream": "4.2.0",
|
||||
"@standard-schema/spec": "1.1.0",
|
||||
@@ -149,9 +149,9 @@
|
||||
"electron"
|
||||
],
|
||||
"overrides": {
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/keymap": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@opentui/core": "0.0.0-20260803-1395a054",
|
||||
"@opentui/keymap": "0.0.0-20260803-1395a054",
|
||||
"@opentui/solid": "0.0.0-20260803-1395a054",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"effect": "catalog:"
|
||||
|
||||
@@ -31,9 +31,9 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opencode-ai/theme": "workspace:*",
|
||||
"@opentui/core": ">=0.4.5",
|
||||
"@opentui/keymap": ">=0.4.5",
|
||||
"@opentui/solid": ">=0.4.5",
|
||||
"@opentui/core": "0.0.0-20260803-1395a054",
|
||||
"@opentui/keymap": "0.0.0-20260803-1395a054",
|
||||
"@opentui/solid": "0.0.0-20260803-1395a054",
|
||||
"solid-js": ">=1.9.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
|
||||
@@ -92,7 +92,6 @@
|
||||
"@opentui/keymap": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"clipboardy": "4.0.0",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"get-east-asian-width": "catalog:",
|
||||
|
||||
@@ -95,6 +95,7 @@ import { destroyRenderer } from "./util/renderer"
|
||||
import { cliErrorMessage, errorFormat } from "./util/error"
|
||||
import { AttentionProvider } from "./context/attention"
|
||||
import { StorageProvider } from "./context/storage"
|
||||
import { createTuiClipboard } from "./clipboard"
|
||||
|
||||
registerOpencodeSpinner()
|
||||
|
||||
@@ -215,9 +216,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
Effect.catch(() => Effect.tryPromise(() => api.location.get())),
|
||||
)
|
||||
const directory = location.directory
|
||||
const pluginDirectories = yield* Effect.promise(() =>
|
||||
tuiPluginDirectories(process.cwd(), global.config),
|
||||
)
|
||||
const pluginDirectories = yield* Effect.promise(() => tuiPluginDirectories(process.cwd(), global.config))
|
||||
const handoff = input.terminalHandoff ? yield* Effect.promise(input.terminalHandoff) : undefined
|
||||
const managed = input.server.service
|
||||
const service = managed
|
||||
@@ -265,6 +264,13 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
(renderer) => Effect.sync(() => destroyRenderer(renderer)),
|
||||
)
|
||||
})
|
||||
const clipboard = yield* Effect.acquireRelease(
|
||||
Effect.sync(() => createTuiClipboard(renderer)),
|
||||
(clipboard) =>
|
||||
Effect.tryPromise(() => clipboard.dispose()).pipe(
|
||||
Effect.catch((error) => Effect.sync(() => log("error", "Failed to dispose TUI clipboard", { error }))),
|
||||
),
|
||||
)
|
||||
win32DisableProcessedInput()
|
||||
const finalizers = new Set<() => Promise<void>>()
|
||||
yield* Effect.addFinalizer(() =>
|
||||
@@ -301,7 +307,11 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
<EpilogueProvider set={(value) => (exit.epilogue = value)}>
|
||||
<TuiAppProvider value={input.app}>
|
||||
<ErrorBoundary
|
||||
fallback={(error, reset) => <ErrorComponent error={error} reset={reset} mode={mode} />}
|
||||
fallback={(error, reset) => (
|
||||
<ClipboardProvider value={clipboard}>
|
||||
<ErrorComponent error={error} reset={reset} mode={mode} />
|
||||
</ClipboardProvider>
|
||||
)}
|
||||
>
|
||||
<TuiPathsProvider
|
||||
value={{
|
||||
@@ -350,7 +360,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
skipInitialLoading: Boolean(process.env.OPENCODE_FAST_BOOT),
|
||||
}}
|
||||
>
|
||||
<ClipboardProvider>
|
||||
<ClipboardProvider value={clipboard}>
|
||||
<ArgsProvider {...input.args}>
|
||||
<ConfigProvider
|
||||
config={config}
|
||||
@@ -519,7 +529,7 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
if (!text || text.length === 0) return
|
||||
|
||||
await clipboard
|
||||
.write?.(text)
|
||||
.write(text)
|
||||
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
|
||||
.catch(toast.error)
|
||||
|
||||
|
||||
+59
-117
@@ -1,124 +1,66 @@
|
||||
import { execFile, spawn } from "node:child_process"
|
||||
import { readFile, rm } from "node:fs/promises"
|
||||
import { platform, release, tmpdir } from "node:os"
|
||||
import path from "node:path"
|
||||
import { promisify } from "node:util"
|
||||
import {
|
||||
createClipboard,
|
||||
createHostClipboard,
|
||||
createRendererClipboardAdapter,
|
||||
decodePasteBytes,
|
||||
type ClipboardService as CoreClipboardService,
|
||||
type RendererClipboardBoundary,
|
||||
} from "@opentui/core"
|
||||
import type { ClipboardContent, ClipboardService } from "./context/clipboard"
|
||||
|
||||
const exec = promisify(execFile)
|
||||
export type OwnedClipboardService = Required<ClipboardService> & Readonly<{ dispose(): Promise<void> }>
|
||||
|
||||
function command(command: string, args: string[] = [], input?: string) {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
const child = spawn(command, args, { stdio: [input === undefined ? "ignore" : "pipe", "pipe", "ignore"] })
|
||||
const output: Buffer[] = []
|
||||
child.on("error", reject)
|
||||
child.stdout?.on("data", (chunk: Buffer) => output.push(chunk))
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) return resolve(Buffer.concat(output))
|
||||
reject(new Error(`${command} exited with code ${code}`))
|
||||
})
|
||||
if (input !== undefined) child.stdin?.end(input)
|
||||
})
|
||||
export function createTuiClipboard(renderer: RendererClipboardBoundary): OwnedClipboardService {
|
||||
return createClipboardAdapter(
|
||||
createClipboard({
|
||||
host: createHostClipboard(),
|
||||
terminal: createRendererClipboardAdapter(renderer),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function writeOsc52(text: string) {
|
||||
if (!process.stdout.isTTY) return
|
||||
const sequence = `\x1b]52;c;${Buffer.from(text).toString("base64")}\x07`
|
||||
process.stdout.write(process.env.TMUX || process.env.STY ? `\x1bPtmux;\x1b${sequence}\x1b\\` : sequence)
|
||||
}
|
||||
|
||||
export async function read() {
|
||||
if (platform() === "darwin") {
|
||||
const file = path.join(tmpdir(), "opencode-clipboard.png")
|
||||
try {
|
||||
await exec("osascript", [
|
||||
"-e",
|
||||
'set imageData to the clipboard as "PNGf"',
|
||||
"-e",
|
||||
`set fileRef to open for access POSIX file "${file}" with write permission`,
|
||||
"-e",
|
||||
"set eof fileRef to 0",
|
||||
"-e",
|
||||
"write imageData to fileRef",
|
||||
"-e",
|
||||
"close access fileRef",
|
||||
])
|
||||
return { data: (await readFile(file)).toString("base64"), mime: "image/png" }
|
||||
} catch {
|
||||
// Fall through to text clipboard.
|
||||
} finally {
|
||||
await rm(file, { force: true }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
if (platform() === "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 image = await command("powershell.exe", ["-NonInteractive", "-NoProfile", "-command", script]).catch(() =>
|
||||
Buffer.alloc(0),
|
||||
)
|
||||
if (image.length) return { data: image.toString().trim(), mime: "image/png" }
|
||||
}
|
||||
|
||||
if (platform() === "linux") {
|
||||
const wayland = await command("wl-paste", ["-t", "image/png"]).catch(() => Buffer.alloc(0))
|
||||
if (wayland.length) return { data: wayland.toString("base64"), mime: "image/png" }
|
||||
const x11 = await command("xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]).catch(() =>
|
||||
Buffer.alloc(0),
|
||||
)
|
||||
if (x11.length) return { data: x11.toString("base64"), mime: "image/png" }
|
||||
}
|
||||
|
||||
const { default: clipboardy } = await import("clipboardy")
|
||||
const text = await clipboardy.read().catch(() => undefined)
|
||||
if (text) return { data: text, mime: "text/plain" }
|
||||
}
|
||||
|
||||
export function copyCommand(
|
||||
os: NodeJS.Platform,
|
||||
wayland: boolean,
|
||||
has: (name: string) => boolean,
|
||||
): string[] | undefined {
|
||||
if (os === "darwin" && has("osascript")) return ["osascript"]
|
||||
if (os === "linux" && wayland && has("wl-copy")) return ["wl-copy"]
|
||||
if (os === "linux" && has("xclip")) return ["xclip", "-selection", "clipboard"]
|
||||
if (os === "linux" && has("xsel")) return ["xsel", "--clipboard", "--input"]
|
||||
if (os === "win32" && has("powershell.exe")) {
|
||||
return [
|
||||
"powershell.exe",
|
||||
"-NonInteractive",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
let copyMethod: Promise<(text: string) => Promise<void>> | undefined
|
||||
|
||||
function getCopyMethod() {
|
||||
return (copyMethod ??= (async () => {
|
||||
const { which } = await import("@opencode-ai/core/util/which")
|
||||
const native = copyCommand(platform(), Boolean(process.env.WAYLAND_DISPLAY), (name) => Boolean(which(name)))
|
||||
if (native?.[0] === "osascript") {
|
||||
return async (text: string) => {
|
||||
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
||||
await command("osascript", ["-e", `set the clipboard to "${escaped}"`]).catch(() => undefined)
|
||||
export function createClipboardAdapter(clipboard: CoreClipboardService): OwnedClipboardService {
|
||||
return {
|
||||
async read(): Promise<ClipboardContent | undefined> {
|
||||
const result = await clipboard.read({
|
||||
preferredTypes: ["image/png", "text/plain"],
|
||||
selection: "clipboard",
|
||||
})
|
||||
if (result.status !== "read") {
|
||||
if (result.status === "failed") throw result.error
|
||||
if (result.status === "timed-out") throw new Error("Clipboard read timed out")
|
||||
if (result.status === "limit-exceeded") {
|
||||
throw new RangeError("Clipboard content exceeded configured read or image conversion limits")
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
if (native) {
|
||||
return async (text: string) => {
|
||||
await command(native[0], native.slice(1), text).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
return async (text: string) => {
|
||||
const { default: clipboardy } = await import("clipboardy")
|
||||
await clipboardy.write(text).catch(() => undefined)
|
||||
}
|
||||
})())
|
||||
}
|
||||
|
||||
export async function write(text: string) {
|
||||
writeOsc52(text)
|
||||
const method = await getCopyMethod()
|
||||
await method(text)
|
||||
if (result.representation.mimeType === "image/png") {
|
||||
return {
|
||||
data: Buffer.from(result.representation.bytes).toString("base64"),
|
||||
mime: result.representation.mimeType,
|
||||
}
|
||||
}
|
||||
if (result.representation.mimeType === "text/plain") {
|
||||
if (result.representation.bytes.length === 0) return undefined
|
||||
return {
|
||||
data: decodePasteBytes(result.representation.bytes),
|
||||
mime: result.representation.mimeType,
|
||||
}
|
||||
}
|
||||
throw new Error(`Unexpected clipboard MIME type: ${result.representation.mimeType}`)
|
||||
},
|
||||
async write(text) {
|
||||
const result = await clipboard.writeText(text, {
|
||||
destination: "all-available",
|
||||
selection: "clipboard",
|
||||
})
|
||||
if (result.host.status === "written" || result.terminal.status === "attempted") return
|
||||
if (result.host.status === "failed") throw result.host.error
|
||||
throw new Error(`Clipboard write failed (host: ${result.host.status}, terminal: ${result.terminal.status})`)
|
||||
},
|
||||
dispose() {
|
||||
return clipboard.dispose()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function DialogDebug() {
|
||||
.map((entry) => `${entry.label}: ${entry.value}`)
|
||||
.join("\n")
|
||||
void clipboard
|
||||
.write?.(text)
|
||||
.write(text)
|
||||
.then(() => {
|
||||
setCopied(true)
|
||||
toast.show({ message: "Debug info copied to clipboard", variant: "info" })
|
||||
|
||||
@@ -452,7 +452,7 @@ function OAuthAuto(props: {
|
||||
run: () => {
|
||||
const value = props.attempt.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? props.attempt.url
|
||||
clipboard
|
||||
.write?.(value)
|
||||
.write(value)
|
||||
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
|
||||
.catch(toast.error)
|
||||
},
|
||||
|
||||
@@ -146,7 +146,6 @@ function DialogMcpError(props: { server: McpServer; onBack: () => void }) {
|
||||
onMount(() => dialog.setSize("large"))
|
||||
|
||||
const copy = () => {
|
||||
if (!clipboard.write) return
|
||||
void clipboard
|
||||
.write(error())
|
||||
.then(() => setCopied(true))
|
||||
|
||||
@@ -12,7 +12,7 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?:
|
||||
const exit = useExit()
|
||||
const clipboard = useClipboard()
|
||||
const app = useTuiApp()
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
const [copyState, setCopyState] = createSignal<"idle" | "copied" | "failed">("idle")
|
||||
|
||||
// Safe fallback palette per mode (mirrors theme/assets/opencode.json) since the
|
||||
// theme context may be the thing that crashed.
|
||||
@@ -46,11 +46,19 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?:
|
||||
const issueURL = buildIssueURL(message, stack, app.version)
|
||||
|
||||
const copyReport = () => {
|
||||
void clipboard.write?.(issueURL.toString()).then(() => setCopied(true))
|
||||
void clipboard
|
||||
.write(issueURL.toString())
|
||||
.then(() => setCopyState("copied"))
|
||||
.catch(() => setCopyState("failed"))
|
||||
}
|
||||
|
||||
const actions = [
|
||||
{ key: "c", label: () => (copied() ? "✓ Copied" : "Copy report"), copy: true, onUse: copyReport },
|
||||
{
|
||||
key: "c",
|
||||
label: () => ({ idle: "Copy report", copied: "✓ Copied", failed: "Copy failed" })[copyState()],
|
||||
copy: true,
|
||||
onUse: copyReport,
|
||||
},
|
||||
{ key: "r", label: () => "Restart", onUse: props.reset },
|
||||
{ key: "q", label: () => "Quit", onUse: () => exit() },
|
||||
]
|
||||
@@ -135,13 +143,20 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?:
|
||||
<For each={actions}>
|
||||
{(action, index) => {
|
||||
const isSelected = () => selected() === index()
|
||||
const isCopied = () => action.copy && copied()
|
||||
const copyColor = () =>
|
||||
action.copy
|
||||
? copyState() === "copied"
|
||||
? colors.success
|
||||
: copyState() === "failed"
|
||||
? colors.error
|
||||
: undefined
|
||||
: undefined
|
||||
return (
|
||||
<box flexDirection="column" alignItems="center" flexShrink={0}>
|
||||
<box
|
||||
onMouseDown={() => setSelected(index())}
|
||||
onMouseUp={() => action.onUse()}
|
||||
backgroundColor={isCopied() ? colors.success : isSelected() ? colors.primary : colors.element}
|
||||
backgroundColor={copyColor() ?? (isSelected() ? colors.primary : colors.element)}
|
||||
minWidth={15}
|
||||
alignItems="center"
|
||||
paddingLeft={2}
|
||||
@@ -149,7 +164,7 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?:
|
||||
>
|
||||
<text
|
||||
attributes={TextAttributes.BOLD}
|
||||
fg={isCopied() || isSelected() ? colors.onPrimary : colors.text}
|
||||
fg={copyColor() || isSelected() ? colors.onPrimary : colors.text}
|
||||
>
|
||||
{action.label()}
|
||||
</text>
|
||||
@@ -189,9 +204,11 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?:
|
||||
<Show when={showFooter()}>
|
||||
<box flexDirection="column" alignItems="center" flexShrink={0}>
|
||||
<text fg={colors.muted}>
|
||||
{copied()
|
||||
{copyState() === "copied"
|
||||
? "Report copied — paste it into a new GitHub issue."
|
||||
: "Copy the report and open a GitHub issue to help us fix this."}
|
||||
: copyState() === "failed"
|
||||
? "Clipboard write failed. Try again or report the crash manually."
|
||||
: "Copy the report and open a GitHub issue to help us fix this."}
|
||||
</text>
|
||||
<text fg={colors.muted}>OpenCode {app.version}</text>
|
||||
</box>
|
||||
|
||||
@@ -379,7 +379,10 @@ export function Prompt(props: PromptProps) {
|
||||
run: async (_input: string | undefined, event?: KeyEvent) => {
|
||||
event?.preventDefault()
|
||||
event?.stopPropagation()
|
||||
const content = await clipboard.read?.()
|
||||
const content = await clipboard.read().catch((error) => {
|
||||
toast.error(error)
|
||||
return undefined
|
||||
})
|
||||
if (content?.mime.startsWith("image/")) {
|
||||
await pasteAttachment({
|
||||
filename: "clipboard",
|
||||
@@ -1320,10 +1323,7 @@ export function Prompt(props: PromptProps) {
|
||||
return `Ask anything... "${list()[store.placeholder % list().length]}"`
|
||||
})()
|
||||
if (!value) return undefined
|
||||
const width =
|
||||
dimensions().width < 44
|
||||
? dimensions().width - 5
|
||||
: Math.min(75, dimensions().width - 4) - 5
|
||||
const width = dimensions().width < 44 ? dimensions().width - 5 : Math.min(75, dimensions().width - 4) - 5
|
||||
return Locale.takeWidth(value, Math.max(1, width)).trimEnd()
|
||||
})
|
||||
const locationLabel = createMemo(() => {
|
||||
@@ -1419,11 +1419,10 @@ export function Prompt(props: PromptProps) {
|
||||
// Windows ConPTY/Terminal often sends CR-only newlines in bracketed paste
|
||||
// Replace CRLF first, then any remaining CR
|
||||
const normalizedText = decodePasteBytes(event.bytes).replace(/\r\n/g, "\n").replace(/\r/g, "\n")
|
||||
const pastedContent = normalizedText.trim()
|
||||
|
||||
// Windows Terminal <1.25 can surface image-only clipboard as an
|
||||
// empty bracketed paste. Windows Terminal 1.25+ does not.
|
||||
if (!pastedContent) {
|
||||
if (event.bytes.byteLength === 0) {
|
||||
keymap.dispatch("prompt.paste")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { createContext, type JSX, useContext } from "solid-js"
|
||||
import { read, write } from "../clipboard"
|
||||
|
||||
export type ClipboardContent = Readonly<{ data: string; mime: string }>
|
||||
export type ClipboardService = Readonly<{
|
||||
read?(): Promise<ClipboardContent | undefined>
|
||||
write?(text: string): Promise<void>
|
||||
read(): Promise<ClipboardContent | undefined>
|
||||
write(text: string): Promise<void>
|
||||
}>
|
||||
const clipboard = { read, write }
|
||||
const ClipboardContext = createContext<ClipboardService>(clipboard)
|
||||
|
||||
export function ClipboardProvider(props: { value?: ClipboardService; children: JSX.Element }) {
|
||||
return <ClipboardContext.Provider value={props.value ?? clipboard}>{props.children}</ClipboardContext.Provider>
|
||||
const ClipboardContext = createContext<ClipboardService>()
|
||||
|
||||
export function ClipboardProvider(props: { value: ClipboardService; children: JSX.Element }) {
|
||||
return <ClipboardContext.Provider value={props.value}>{props.children}</ClipboardContext.Provider>
|
||||
}
|
||||
|
||||
export function useClipboard() {
|
||||
return useContext(ClipboardContext)
|
||||
const value = useContext(ClipboardContext)
|
||||
if (!value) throw new Error("useClipboard must be used within a ClipboardProvider")
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -66,8 +66,12 @@ export function DialogMessage(props: {
|
||||
: "text" in value
|
||||
? value.text
|
||||
: ""
|
||||
await clipboard.write?.(text)
|
||||
dialog.clear()
|
||||
try {
|
||||
await clipboard.write(text)
|
||||
dialog.clear()
|
||||
} catch (error) {
|
||||
toast.error(error)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -364,7 +364,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
|
||||
function copyExternal() {
|
||||
const current = externalField()
|
||||
if (!current || !clipboard.write) return
|
||||
if (!current) return
|
||||
void clipboard
|
||||
.write(current.url)
|
||||
.then(() => {
|
||||
@@ -992,7 +992,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
>
|
||||
enter <span style={{ fg: theme.text.subdued }}>{actionLabel()}</span>
|
||||
</text>
|
||||
<Show when={externalField() && clipboard.write}>
|
||||
<Show when={externalField()}>
|
||||
<text fg={theme.text.default} onMouseUp={copyExternal}>
|
||||
c <span style={{ fg: theme.text.subdued }}>copy</span>
|
||||
</text>
|
||||
|
||||
@@ -779,7 +779,7 @@ export function Session() {
|
||||
}
|
||||
|
||||
clipboard
|
||||
.write?.(text)
|
||||
.write(text)
|
||||
.then(() => toast.show({ message: "Message copied to clipboard!", variant: "success" }))
|
||||
.catch(() => toast.show({ message: "Failed to copy to clipboard", variant: "error" }))
|
||||
dialog.clear()
|
||||
@@ -797,7 +797,7 @@ export function Session() {
|
||||
const sessionData = session()
|
||||
if (!sessionData) return
|
||||
const transcript = formatSessionTranscript(sessionData, messages(), showThinking())
|
||||
await clipboard.write?.(transcript)
|
||||
await clipboard.write(transcript)
|
||||
toast.show({ message: "Session transcript copied to clipboard!", variant: "success" })
|
||||
} catch {
|
||||
toast.show({ message: "Failed to copy session transcript", variant: "error" })
|
||||
@@ -840,7 +840,7 @@ export function Session() {
|
||||
})()
|
||||
|
||||
if (options.action === "copy") {
|
||||
await clipboard.write?.(content)
|
||||
await clipboard.write(content)
|
||||
dialog.clear()
|
||||
toast.show({ message: "Copied to clipboard", variant: "success" })
|
||||
return
|
||||
|
||||
@@ -204,7 +204,7 @@ export function DialogProvider(props: ParentProps) {
|
||||
|
||||
function copySelection() {
|
||||
const text = renderer.getSelection()?.getSelectedText()
|
||||
if (!text || !clipboard.write) return false
|
||||
if (!text) return false
|
||||
void clipboard.write(text).then(
|
||||
() => toast.show({ message: "Copied to clipboard", variant: "info" }),
|
||||
(error) => toast.error(error),
|
||||
|
||||
@@ -35,7 +35,7 @@ export function copy(renderer: Renderer, toast: Toast, clipboard: ClipboardServi
|
||||
focus?.getClipboardText && selection.selectedRenderables.includes(focus) ? focus.getClipboardText(text) : text
|
||||
|
||||
clipboard
|
||||
?.write?.(clipboardText)
|
||||
.write(clipboardText)
|
||||
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
|
||||
.catch(toast.error)
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ async function mountForm(root: string, width = 80) {
|
||||
>
|
||||
<ClipboardProvider
|
||||
value={{
|
||||
async read() {
|
||||
return undefined
|
||||
},
|
||||
write(text) {
|
||||
copied.push(text)
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -1,19 +1,129 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { copyCommand } from "../src/clipboard"
|
||||
import {
|
||||
createClipboard,
|
||||
type ClipboardReadOptions,
|
||||
type ClipboardReadResult,
|
||||
type ClipboardService as CoreClipboardService,
|
||||
type ClipboardWriteOptions,
|
||||
type ClipboardWriteResult,
|
||||
type HostClipboardService,
|
||||
} from "@opentui/core"
|
||||
import { createClipboardAdapter } from "../src/clipboard"
|
||||
|
||||
test("prefers Wayland clipboard when available", () => {
|
||||
expect(copyCommand("linux", true, (name) => name === "wl-copy")).toEqual(["wl-copy"])
|
||||
function coreClipboard(options: {
|
||||
read?: ClipboardReadResult
|
||||
write?: ClipboardWriteResult
|
||||
onRead?: (input: ClipboardReadOptions) => void
|
||||
onWrite?: (text: string, input: ClipboardWriteOptions) => void
|
||||
}): CoreClipboardService {
|
||||
return {
|
||||
async read(input) {
|
||||
options.onRead?.(input)
|
||||
return options.read ?? { status: "empty" }
|
||||
},
|
||||
async writeText(text, input) {
|
||||
options.onWrite?.(text, input)
|
||||
return (
|
||||
options.write ?? {
|
||||
host: { status: "written" },
|
||||
terminal: { status: "not-attempted", capability: "unknown" },
|
||||
}
|
||||
)
|
||||
},
|
||||
async clear() {
|
||||
return {
|
||||
host: { status: "cleared" },
|
||||
terminal: { status: "not-attempted", capability: "unknown" },
|
||||
}
|
||||
},
|
||||
async dispose() {},
|
||||
}
|
||||
}
|
||||
|
||||
test("adapts OpenTUI image and text reads", async () => {
|
||||
const requests: ClipboardReadOptions[] = []
|
||||
const image = createClipboardAdapter(
|
||||
coreClipboard({
|
||||
read: {
|
||||
status: "read",
|
||||
representation: { mimeType: "image/png", bytes: new Uint8Array([0, 1, 2, 255]) },
|
||||
},
|
||||
onRead: (input) => requests.push(input),
|
||||
}),
|
||||
)
|
||||
const text = "line 1\r\n\t世界"
|
||||
const plain = createClipboardAdapter(
|
||||
coreClipboard({
|
||||
read: {
|
||||
status: "read",
|
||||
representation: { mimeType: "text/plain", bytes: new TextEncoder().encode(text) },
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(await image.read()).toEqual({ data: "AAEC/w==", mime: "image/png" })
|
||||
expect(await plain.read()).toEqual({ data: text, mime: "text/plain" })
|
||||
expect(requests).toEqual([{ preferredTypes: ["image/png", "text/plain"], selection: "clipboard" }])
|
||||
})
|
||||
|
||||
test("uses osascript on macOS", () => {
|
||||
expect(copyCommand("darwin", false, (name) => name === "osascript")).toEqual(["osascript"])
|
||||
test("uses all available routes but skips the process host remotely", async () => {
|
||||
const writes = { host: 0, terminal: 0 }
|
||||
const host: HostClipboardService = {
|
||||
maxWriteBytes: 8 * 1024 * 1024,
|
||||
async read() {
|
||||
return { status: "empty" }
|
||||
},
|
||||
async writeText() {
|
||||
writes.host++
|
||||
return { status: "written" }
|
||||
},
|
||||
async clear() {
|
||||
return { status: "cleared" }
|
||||
},
|
||||
async dispose() {},
|
||||
}
|
||||
const clipboard = createClipboardAdapter(
|
||||
createClipboard({
|
||||
host,
|
||||
terminal: {
|
||||
remote: true,
|
||||
writeText() {
|
||||
writes.terminal++
|
||||
return { status: "attempted", capability: "supported" }
|
||||
},
|
||||
clear() {
|
||||
return { status: "attempted", capability: "supported" }
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(await clipboard.write("hello")).toBeUndefined()
|
||||
expect(writes).toEqual({ host: 0, terminal: 1 })
|
||||
})
|
||||
|
||||
test("falls back through X11 clipboard commands", () => {
|
||||
expect(copyCommand("linux", true, (name) => name === "xclip")).toEqual(["xclip", "-selection", "clipboard"])
|
||||
expect(copyCommand("linux", false, (name) => name === "xsel")).toEqual(["xsel", "--clipboard", "--input"])
|
||||
})
|
||||
test("rejects only when no clipboard route accepted the write", async () => {
|
||||
const writes: [string, ClipboardWriteOptions][] = []
|
||||
const failure = new Error("native clipboard failed")
|
||||
const fallback = createClipboardAdapter(
|
||||
coreClipboard({
|
||||
write: {
|
||||
host: { status: "written" },
|
||||
terminal: { status: "local-failure", capability: "supported" },
|
||||
},
|
||||
}),
|
||||
)
|
||||
const clipboard = createClipboardAdapter(
|
||||
coreClipboard({
|
||||
write: {
|
||||
host: { status: "failed", error: failure },
|
||||
terminal: { status: "local-failure", capability: "supported" },
|
||||
},
|
||||
onWrite: (text, input) => writes.push([text, input]),
|
||||
}),
|
||||
)
|
||||
|
||||
test("returns undefined when native clipboard is unavailable", () => {
|
||||
expect(copyCommand("linux", false, () => false)).toBeUndefined()
|
||||
expect(await fallback.write("hello")).toBeUndefined()
|
||||
expect(await clipboard.write("hello").then(undefined, (error) => error)).toBe(failure)
|
||||
expect(writes).toEqual([["hello", { destination: "all-available", selection: "clipboard" }]])
|
||||
})
|
||||
|
||||
@@ -7,6 +7,14 @@ import {
|
||||
} from "../../src/context/runtime"
|
||||
import type { ParentProps } from "solid-js"
|
||||
import { LogProvider, type LogSink } from "../../src/context/log"
|
||||
import { ClipboardProvider, type ClipboardService } from "../../src/context/clipboard"
|
||||
|
||||
const clipboard: ClipboardService = {
|
||||
async read() {
|
||||
return undefined
|
||||
},
|
||||
async write() {},
|
||||
}
|
||||
|
||||
export function TestTuiContexts(
|
||||
props: ParentProps<{
|
||||
@@ -14,6 +22,7 @@ export function TestTuiContexts(
|
||||
directory?: string
|
||||
paths?: Partial<TuiPaths>
|
||||
log?: LogSink
|
||||
clipboard?: ClipboardService
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
@@ -28,7 +37,9 @@ export function TestTuiContexts(
|
||||
}}
|
||||
>
|
||||
<TuiTerminalEnvironmentProvider value={{ platform: "linux" }}>
|
||||
<TuiStartupProvider value={{ skipInitialLoading: false }}>{props.children}</TuiStartupProvider>
|
||||
<TuiStartupProvider value={{ skipInitialLoading: false }}>
|
||||
<ClipboardProvider value={props.clipboard ?? clipboard}>{props.children}</ClipboardProvider>
|
||||
</TuiStartupProvider>
|
||||
</TuiTerminalEnvironmentProvider>
|
||||
</TuiPathsProvider>
|
||||
</LogProvider>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { afterAll, expect, mock, test } from "bun:test"
|
||||
import { TextareaRenderable, type HostClipboardService } from "@opentui/core"
|
||||
import { createTestRenderer } from "@opentui/core/testing"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Effect, FileSystem } from "effect"
|
||||
import { createComponent } from "solid-js"
|
||||
import { Prompt } from "../../src/component/prompt"
|
||||
import { createEventStream, createFetch } from "../fixture/tui-client"
|
||||
|
||||
const openTui = { ...(await import("@opentui/core")) }
|
||||
let activeSetup: Awaited<ReturnType<typeof createTestRenderer>> | undefined
|
||||
let reads = 0
|
||||
|
||||
await mock.module("@opentui/core", () => ({
|
||||
...openTui,
|
||||
createCliRenderer: async () => {
|
||||
if (!activeSetup) throw new Error("Prompt renderer is not mounted")
|
||||
return activeSetup.renderer
|
||||
},
|
||||
createHostClipboard: () =>
|
||||
({
|
||||
maxWriteBytes: 8 * 1024 * 1024,
|
||||
async read() {
|
||||
reads++
|
||||
return { status: "empty" }
|
||||
},
|
||||
async writeText() {
|
||||
return { status: "written" }
|
||||
},
|
||||
async clear() {
|
||||
return { status: "cleared" }
|
||||
},
|
||||
async dispose() {},
|
||||
}) satisfies HostClipboardService,
|
||||
}))
|
||||
await mock.module("../../src/routes/home", () => ({
|
||||
Home: () => createComponent(Prompt, { showPlaceholder: false }),
|
||||
}))
|
||||
const { run } = await import("../../src/app")
|
||||
|
||||
afterAll(() => mock.restore())
|
||||
|
||||
test("only zero-byte terminal pastes read the host clipboard", async () => {
|
||||
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
||||
activeSetup = setup
|
||||
reads = 0
|
||||
const mounted = Promise.withResolvers<void>()
|
||||
const setTitle = setup.renderer.setTerminalTitle.bind(setup.renderer)
|
||||
setup.renderer.setTerminalTitle = (title) => {
|
||||
if (title === "OpenCode") mounted.resolve()
|
||||
setTitle(title)
|
||||
}
|
||||
const events = createEventStream()
|
||||
const calls = createFetch(undefined, events)
|
||||
const preloaded = Promise.withResolvers<void>()
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch: async (request) => {
|
||||
const response = await calls.fetch(request)
|
||||
if (new URL(request.url).pathname === "/api/session/active") preloaded.resolve()
|
||||
return response
|
||||
},
|
||||
})
|
||||
const task = Effect.runPromise(
|
||||
run({
|
||||
app: { name: "test", version: "test", channel: "test" },
|
||||
server: { endpoint: { url: server.url.toString() } },
|
||||
config: { get: async () => ({ prompt: { paste: "full" as const } }), update: async () => ({}) },
|
||||
packages: { resolve: async () => undefined },
|
||||
args: {},
|
||||
log: () => {},
|
||||
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node)), Effect.provide(FileSystem.layerNoop({}))),
|
||||
)
|
||||
|
||||
try {
|
||||
await mounted.promise
|
||||
await setup.waitFor(() => setup.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
||||
await preloaded.promise
|
||||
await Bun.sleep(0)
|
||||
const input = setup.renderer.currentFocusedEditor
|
||||
if (!(input instanceof TextareaRenderable)) throw new Error("Prompt textarea is not focused")
|
||||
|
||||
await setup.mockInput.pasteBracketedText(" \t\n")
|
||||
await setup.waitFor(() => input.plainText === " \t\n")
|
||||
expect(reads).toBe(0)
|
||||
|
||||
setup.renderer.keyInput.processPaste(new Uint8Array())
|
||||
await setup.waitFor(() => reads === 1)
|
||||
expect(input.plainText).toBe(" \t\n")
|
||||
} finally {
|
||||
setup.renderer.destroy()
|
||||
await task
|
||||
await server.stop()
|
||||
activeSetup = undefined
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user