diff --git a/bun.lock b/bun.lock index 50e72c9840..12dc5a491d 100644 --- a/bun.lock +++ b/bun.lock @@ -613,9 +613,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-20260808-9ecf7c0a", + "@opentui/keymap": "0.0.0-20260808-9ecf7c0a", + "@opentui/solid": "0.0.0-20260808-9ecf7c0a", "solid-js": ">=1.9.0", }, "optionalPeers": [ @@ -903,7 +903,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:", @@ -1089,9 +1088,9 @@ "@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch", }, "overrides": { - "@opentui/core": "catalog:", - "@opentui/keymap": "catalog:", - "@opentui/solid": "catalog:", + "@opentui/core": "0.0.0-20260808-9ecf7c0a", + "@opentui/keymap": "0.0.0-20260808-9ecf7c0a", + "@opentui/solid": "0.0.0-20260808-9ecf7c0a", "@types/bun": "catalog:", "@types/node": "catalog:", "effect": "catalog:", @@ -1109,9 +1108,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-20260808-9ecf7c0a", + "@opentui/keymap": "0.0.0-20260808-9ecf7c0a", + "@opentui/solid": "0.0.0-20260808-9ecf7c0a", "@pierre/diffs": "1.2.10", "@playwright/test": "1.59.1", "@sentry/solid": "10.36.0", @@ -2169,27 +2168,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-20260808-9ecf7c0a", "", { "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-20260808-9ecf7c0a", "@opentui/core-darwin-x64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-linux-arm64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-linux-arm64-musl": "0.0.0-20260808-9ecf7c0a", "@opentui/core-linux-x64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-linux-x64-musl": "0.0.0-20260808-9ecf7c0a", "@opentui/core-win32-arm64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-win32-x64": "0.0.0-20260808-9ecf7c0a" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-y9MKL8LMup4ebW3uZ57RSsPyOv37sIZxuWiHq4XJK9wrzpzTMnVomzxpWOI0S/QtLH2QzZ7znnbF+IuvIQ1hLQ=="], - "@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-20260808-9ecf7c0a", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Bn6fUZrwbUojkJG4YHowg/h/u3SPANmEivrl8YVbxBPCYOhZzQWxAVf7Fwg6PD0BpWk5gt9inQrir2DzWh5UpA=="], - "@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-20260808-9ecf7c0a", "", { "os": "darwin", "cpu": "x64" }, "sha512-lrVskzbjcT9yJCUgjOpvPpEq2ipmuDyHTMJ+OebsJFdATdryJLzRAr5BZiFGXEo4CJ2CS/+6vT+ki7dmhpM+0g=="], - "@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-20260808-9ecf7c0a", "", { "os": "linux", "cpu": "arm64" }, "sha512-czqIqdnBvyFRSJ0BEdNn7moRN0KQo5gvhSsOSfrzueHxsktgWcmk1cMeCvTkzC3JCBvCwlB1KnSdt4jlwNO1EQ=="], - "@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-20260808-9ecf7c0a", "", { "os": "linux", "cpu": "arm64" }, "sha512-6nixv6wlgjelUVJVHkJsCpuARb1rE/oxhIIthyLd062u5veoBt83cRGH9KmcZiwowKvso58hXn2KC21qAfylhQ=="], - "@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-20260808-9ecf7c0a", "", { "os": "linux", "cpu": "x64" }, "sha512-cxJMfrSsMsrZdU1Ke6awrxz0rdIJuI34l8Le3QlUbM5yLpp09ygJ7CElILMT50g3F+EnRf6BKvVmkbRrDzFQ6g=="], - "@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-20260808-9ecf7c0a", "", { "os": "linux", "cpu": "x64" }, "sha512-7zVbZ0XgCDi3yImmd9Cj+e0WhSuQzWo1ziSQiFgPCR8cs/dfrpYLgBlX9PP0gFOcJKeRccJJv7rnBetl3CB7tg=="], - "@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-20260808-9ecf7c0a", "", { "os": "win32", "cpu": "arm64" }, "sha512-K3SNYJDvB7XD5nxi8jt4jWtgmqPG0Qbob0Px9afqMH3g/R5hIoi2P3GDbqn8JNv82c4dPIGJjXXuNhHa0tZQbw=="], - "@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-20260808-9ecf7c0a", "", { "os": "win32", "cpu": "x64" }, "sha512-3JCpPS8+Gz3PWlQHiFwqYuJ0AM0vKlfbe4+O+0m2EYej/QjN5KjL1nBfVLq5XDj4dVPEIktQKN6e//YzSzdaqA=="], - "@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-20260808-9ecf7c0a", "", { "dependencies": { "@opentui/core": "0.0.0-20260808-9ecf7c0a" }, "peerDependencies": { "@opentui/react": "0.0.0-20260808-9ecf7c0a", "@opentui/solid": "0.0.0-20260808-9ecf7c0a", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-aGxw6P0RJSuUU3y3QtAnKa1B6tQShxR0mjmnN3Cadr4LoduSuWn/l1kE+27KwH0ap9WTAeGVsN7u2oawB9eWKw=="], - "@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-20260808-9ecf7c0a", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20260808-9ecf7c0a", "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-Q7ouv2KxZKO0/Sh0RbhNd7kEw6E7tlrU+yw36xxlnyXOqlK3TkQ+u1RMSZulYAn8mP20pnYLM9OxY4pOC/wK8Q=="], "@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="], @@ -3539,7 +3538,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=="], @@ -3623,8 +3622,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=="], @@ -4041,7 +4038,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=="], @@ -4323,7 +4320,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=="], @@ -4467,8 +4464,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=="], @@ -4807,7 +4802,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=="], @@ -4933,7 +4928,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=="], @@ -4959,7 +4954,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=="], @@ -5561,7 +5556,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=="], @@ -5593,8 +5588,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=="], @@ -6533,8 +6526,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=="], @@ -6719,9 +6710,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=="], @@ -6797,8 +6790,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=="], @@ -7565,20 +7556,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=="], @@ -8639,8 +8616,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=="], diff --git a/package.json b/package.json index 3ec53a2d4c..e5698fca30 100644 --- a/package.json +++ b/package.json @@ -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-20260808-9ecf7c0a", + "@opentui/keymap": "0.0.0-20260808-9ecf7c0a", + "@opentui/solid": "0.0.0-20260808-9ecf7c0a", "@tanstack/solid-virtual": "3.13.32", "@shikijs/stream": "4.2.0", "@standard-schema/spec": "1.1.0", @@ -150,9 +150,9 @@ "electron" ], "overrides": { - "@opentui/core": "catalog:", - "@opentui/keymap": "catalog:", - "@opentui/solid": "catalog:", + "@opentui/core": "0.0.0-20260808-9ecf7c0a", + "@opentui/keymap": "0.0.0-20260808-9ecf7c0a", + "@opentui/solid": "0.0.0-20260808-9ecf7c0a", "@types/bun": "catalog:", "@types/node": "catalog:", "effect": "catalog:" diff --git a/packages/plugin/package.json b/packages/plugin/package.json index c3c44c648d..add97ba1ae 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -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-20260808-9ecf7c0a", + "@opentui/keymap": "0.0.0-20260808-9ecf7c0a", + "@opentui/solid": "0.0.0-20260808-9ecf7c0a", "solid-js": ">=1.9.0" }, "peerDependenciesMeta": { diff --git a/packages/tui/package.json b/packages/tui/package.json index ab3ebc89c9..cc97e9ca64 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -93,7 +93,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:", diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index c4837c9f20..94aa8edddf 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -97,6 +97,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() @@ -251,6 +252,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>() yield* Effect.addFinalizer(() => @@ -287,7 +295,11 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { (exit.epilogue = value)}> } + fallback={(error, reset) => ( + + + + )} > - + toast.show({ message: "Copied to clipboard", variant: "info" })) .catch(toast.error) diff --git a/packages/tui/src/clipboard.ts b/packages/tui/src/clipboard.ts index 2ae29da888..cd7a6ce1a1 100644 --- a/packages/tui/src/clipboard.ts +++ b/packages/tui/src/clipboard.ts @@ -1,125 +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 & Readonly<{ dispose(): Promise }> -function command(command: string, args: string[] = [], input?: string) { - return new Promise((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` - const passthrough = `\x1bPtmux;\x1b${sequence}\x1b\\` - process.stdout.write(process.env.TMUX ? sequence + passthrough : process.env.STY ? passthrough : 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> | 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 { + 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() + }, + } } diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx index 2cbc6c6452..47a45a770a 100644 --- a/packages/tui/src/component/dialog-config.tsx +++ b/packages/tui/src/component/dialog-config.tsx @@ -84,6 +84,15 @@ export const settings: Setting[] = [ values: ["none", "auto"], keywords: ["transcript", "messages"], }, + { + title: "Transcript images", + category: "Session", + path: ["session", "image_preview"], + default: false, + values: [false, true], + labels: ["off", "on"], + keywords: ["attachments", "images", "tool output"], + }, { title: "Enabled", category: "Tabs", @@ -188,6 +197,15 @@ export const settings: Setting[] = [ values: ["compact", "full"], keywords: ["paste summary", "clipboard", "pasted content"], }, + { + title: "Image previews", + category: "Input", + path: ["prompt", "image_preview"], + default: false, + values: [false, true], + labels: ["off", "on"], + keywords: ["attachments", "clipboard", "images", "prompt"], + }, { title: "Leader timeout", category: "Input", diff --git a/packages/tui/src/component/dialog-debug.tsx b/packages/tui/src/component/dialog-debug.tsx index fcf2673187..4419578a5e 100644 --- a/packages/tui/src/component/dialog-debug.tsx +++ b/packages/tui/src/component/dialog-debug.tsx @@ -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" }) diff --git a/packages/tui/src/component/dialog-image-preview.tsx b/packages/tui/src/component/dialog-image-preview.tsx new file mode 100644 index 0000000000..42fdfa7815 --- /dev/null +++ b/packages/tui/src/component/dialog-image-preview.tsx @@ -0,0 +1,71 @@ +import { TextAttributes } from "@opentui/core" +import { useTerminalDimensions } from "@opentui/solid" +import { createMemo, createSignal } from "solid-js" +import { Keymap } from "../context/keymap" +import { useTheme } from "../context/theme" +import { useDialog } from "../ui/dialog" + +type ImagePreviewItem = Readonly<{ + uri: string + mention?: Readonly<{ text: string }> +}> + +export function DialogImagePreview(props: { images: readonly ImagePreviewItem[]; initial: number }) { + const dialog = useDialog() + const dimensions = useTerminalDimensions() + const theme = useTheme("elevated") + const [index, setIndex] = createSignal(Math.max(0, Math.min(props.images.length - 1, props.initial))) + const [failed, setFailed] = createSignal(false) + const current = createMemo(() => props.images[index()]) + const imageHeight = createMemo(() => Math.max(3, dimensions().height - 8)) + + dialog.setSize("xlarge") + dialog.setCentered(true) + + function move(direction: number) { + if (props.images.length < 2) return + setFailed(false) + setIndex((value) => (value + direction + props.images.length) % props.images.length) + } + + Keymap.createLayer(() => ({ + mode: "modal", + commands: [ + { bind: "left", title: "Previous image", group: "Dialog", run: () => move(-1) }, + { bind: "right", title: "Next image", group: "Dialog", run: () => move(1) }, + ], + })) + + return ( + + + + Image {index() + 1} of {props.images.length} + + dialog.clear()}> + esc + + + setFailed(true)} + /> + + move(-1)}> + {props.images.length > 1 ? "← previous" : ""} + + + {failed() ? "No preview" : (current().mention?.text ?? `Image ${index() + 1}`)} + + move(1)}> + {props.images.length > 1 ? "next →" : ""} + + + + ) +} diff --git a/packages/tui/src/component/dialog-integration.tsx b/packages/tui/src/component/dialog-integration.tsx index 30c81e50ce..81f5db9320 100644 --- a/packages/tui/src/component/dialog-integration.tsx +++ b/packages/tui/src/component/dialog-integration.tsx @@ -492,7 +492,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) }, diff --git a/packages/tui/src/component/dialog-mcp.tsx b/packages/tui/src/component/dialog-mcp.tsx index 086e945080..a2999a081e 100644 --- a/packages/tui/src/component/dialog-mcp.tsx +++ b/packages/tui/src/component/dialog-mcp.tsx @@ -172,7 +172,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)) diff --git a/packages/tui/src/component/error-component.tsx b/packages/tui/src/component/error-component.tsx index d500e36c0d..0949e1f904 100644 --- a/packages/tui/src/component/error-component.tsx +++ b/packages/tui/src/component/error-component.tsx @@ -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?: {(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 ( 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?: > {action.label()} @@ -189,9 +204,11 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?: - {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."} OpenCode {app.version} diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index fa5ffbe41d..4303432db1 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -7,9 +7,8 @@ import { decodePasteBytes, type KeyEvent, } from "@opentui/core" -import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js" +import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match, For } from "solid-js" import path from "path" -import { fileURLToPath } from "url" import { useLocal } from "../../context/local" import { useTheme, useThemes } from "../../context/theme" import { tint } from "../../theme/color" @@ -48,13 +47,20 @@ import { DialogSkill } from "../dialog-skill" import { useArgs } from "../../context/args" import { useConfig } from "../../config" import { usePromptMove } from "./move" -import { readLocalAttachment } from "./local-attachment" +import { + normalizePastedFilepath, + parsePastedFilepaths, + readLocalAttachment, + MAX_LOCAL_ATTACHMENT_BYTES, + type LocalAttachment, +} from "./local-attachment" import { useData } from "../../context/data" import { useLocation } from "../../context/location" import { Keymap, type KeymapCommand } from "../../context/keymap" import { abbreviateHome } from "../../runtime" import { PluginSlot } from "../../plugin/render" import type { SessionPending } from "@opencode-ai/schema/session-pending" +import { DialogImagePreview } from "../dialog-image-preview" export type PromptProps = { sessionID?: string @@ -72,17 +78,6 @@ export type PromptProps = { } } -function pastedFilepath(value: string, platform: string) { - const raw = value.replace(/^['"]+|['"]+$/g, "") - if (raw.startsWith("file://")) { - try { - return fileURLToPath(raw) - } catch {} - } - if (platform === "win32") return raw - return raw.replace(/\\(.)/g, "$1") -} - export type PromptRef = { focused: boolean current: PromptInfo @@ -312,6 +307,41 @@ export function Prompt(props: PromptProps) { extmarkToPart: new Map(), interrupt: 0, }) + let disposed = false + let pasteQueue = Promise.resolve() + + function enqueuePaste(run: (changed: () => boolean) => Promise) { + pasteQueue = pasteQueue + .then(async () => { + if (disposed || input.isDestroyed) return + const before = { sessionID: props.sessionID, mode: store.mode, text: input.plainText } + await run( + () => + disposed || + input.isDestroyed || + props.sessionID !== before.sessionID || + store.mode !== before.mode || + input.plainText !== before.text, + ) + }) + .catch((error) => { + if (!disposed) toast.error(error) + }) + return pasteQueue + } + + const imageAttachments = createMemo(() => + (store.prompt.files ?? []).filter((file) => typeof file.uri === "string" && file.uri.startsWith("data:image/")), + ) + const imagePreviewHeight = createMemo(() => Math.max(4, Math.min(8, Math.floor(dimensions().height / 4)))) + const imagePreviewWidth = createMemo(() => imagePreviewHeight() * 2) + const visibleImageAttachments = createMemo(() => imageAttachments().slice(0, 3)) + + function openImagePreview(initial: number) { + const images = imageAttachments() + if (images.length === 0) return + dialog.replace(() => ) + } createEffect( on( @@ -391,22 +421,32 @@ export function Prompt(props: PromptProps) { name: "prompt.paste", category: "Prompt", palette: undefined, - run: async (_input: string | undefined, event?: KeyEvent) => { + run: (_input: string | undefined, event?: KeyEvent) => { event?.preventDefault() event?.stopPropagation() - const content = await clipboard.read?.() - if (content?.mime.startsWith("image/")) { - await pasteAttachment({ - filename: "clipboard", - uri: `data:${content.mime};base64,${content.data}`, - }) - return - } - if (content?.mime === "text/plain") { - await pasteInputText(content.data) - } + return enqueuePaste(async (changed) => { + const content = await clipboard.read() + if (changed()) return + if (content?.mime.startsWith("image/")) { + pasteAttachment({ + filename: "clipboard", + uri: `data:${content.mime};base64,${content.data}`, + }) + return + } + if (content?.mime === "text/plain") { + await pasteInputText(content.data, changed) + } + }) }, }, + { + title: "View image attachments", + name: "prompt.images.view", + category: "Prompt", + enabled: imageAttachments().length > 0, + run: () => openImagePreview(0), + }, { title: "Interrupt session", name: "session.interrupt", @@ -561,6 +601,7 @@ export function Prompt(props: PromptProps) { "prompt.submit", "prompt.editor", "prompt.editor_context.clear", + "prompt.images.view", "prompt.stash", "prompt.stash.pop", "prompt.stash.list", @@ -614,6 +655,7 @@ export function Prompt(props: PromptProps) { }) onCleanup(() => { + disposed = true if (store.prompt.text) { stashed = { prompt: unwrap(store.prompt), cursor: input.cursorOffset } } @@ -1262,27 +1304,39 @@ export function Prompt(props: PromptProps) { return true } - async function pasteInputText(text: string) { + async function pasteInputText(text: string, changed: () => boolean) { const normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n") const pastedContent = normalizedText.trim() - const filepath = pastedFilepath(pastedContent, terminalEnvironment.platform) + const filepath = normalizePastedFilepath(pastedContent, terminalEnvironment.platform) const isUrl = /^(https?):\/\//.test(filepath) if (!isUrl) { const attachment = await readLocalAttachment(filepath) - const filename = path.basename(filepath) - if (attachment?.type === "text") { - pasteText(attachment.content, `[SVG: ${filename ?? "image"}]`) + if (attachment) { + if (changed()) return + pasteLocalAttachment(filepath, attachment) return } - if (attachment?.type === "binary") { - await pasteAttachment({ - filename, - uri: `data:${attachment.mime};base64,${Buffer.from(attachment.content).toString("base64")}`, - }) - return + + const filepaths = parsePastedFilepaths(pastedContent, terminalEnvironment.platform) + if (filepaths.length > 1) { + let remaining = MAX_LOCAL_ATTACHMENT_BYTES + const attachments: Array<{ filepath: string; attachment: LocalAttachment }> = [] + for (const candidate of filepaths) { + const next = await readLocalAttachment(candidate, remaining) + if (!next) break + remaining -= typeof next.content === "string" ? Buffer.byteLength(next.content) : next.content.byteLength + attachments.push({ filepath: candidate, attachment: next }) + } + if (attachments.length === filepaths.length) { + if (changed()) return + for (const item of attachments) pasteLocalAttachment(item.filepath, item.attachment) + return + } } } + if (changed()) return + const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1 if ((lineCount >= 3 || pastedContent.length > 150) && config.prompt?.paste !== "full") { const extmark = input.extmarks.getAllForTypeId(promptPartTypeId).find((extmark) => { @@ -1307,12 +1361,27 @@ export function Prompt(props: PromptProps) { }, 0) } - async function pasteAttachment(file: { filename?: string; uri: string }) { + function pasteLocalAttachment(filepath: string, attachment: LocalAttachment) { + const filename = path.basename(filepath) + if (attachment.type === "text") { + pasteText(attachment.content, `[SVG: ${filename || "image"}]`) + return + } + pasteAttachment({ + filename, + uri: `data:${attachment.mime};base64,${Buffer.from(attachment.content).toString("base64")}`, + }) + } + + function pasteAttachment(file: { filename?: string; uri: string }) { const currentOffset = input.cursorOffset const extmarkStart = currentOffset const pdf = file.uri.startsWith("data:application/pdf;") - const prefix = pdf ? "data:application/pdf;" : "data:image/" - const count = store.prompt.files?.filter((attachment) => attachment.uri.startsWith(prefix)).length ?? 0 + const count = pdf + ? (store.prompt.files?.filter( + (attachment) => typeof attachment.uri === "string" && attachment.uri.startsWith("data:application/pdf;"), + ).length ?? 0) + : imageAttachments().length const virtualText = pdf ? `[PDF ${count + 1}]` : `[Image ${count + 1}]` const extmarkEnd = extmarkStart + virtualText.length const textToInsert = virtualText + " " @@ -1344,7 +1413,6 @@ export function Prompt(props: PromptProps) { draft.extmarkToPart.set(extmarkId, { type: "file", index }) }), ) - return } function clearPrompt() { @@ -1468,6 +1536,74 @@ export function Prompt(props: PromptProps) { flexGrow={1} width="100%" > + 0}> + + + {(file, index) => { + const [failed, setFailed] = createSignal(false) + return ( + { + if (event.button !== 0) return + event.stopPropagation() + openImagePreview(index()) + }} + > + + No preview + + } + > + setFailed(true)} + /> + + + ) + }} + + visibleImageAttachments().length}> + { + if (event.button !== 0) return + event.stopPropagation() + openImagePreview(visibleImageAttachments().length) + }} + > + + +{imageAttachments().length - visibleImageAttachments().length} more + + + + +