diff --git a/.changeset/40f76543.md b/.changeset/40f76543.md new file mode 100644 index 0000000..9f60beb --- /dev/null +++ b/.changeset/40f76543.md @@ -0,0 +1,5 @@ +--- +"@mynameistito/opencode-doomscroll": minor +--- + +Add an experimental GUI plugin that opens X's home timeline while an OpenCode session runs and closes the tab when the run ends. diff --git a/bun.lock b/bun.lock index 4583a61..6be7b1b 100644 --- a/bun.lock +++ b/bun.lock @@ -43,6 +43,21 @@ "typescript": "catalog:", }, }, + "packages/opencode-doomscroll": { + "name": "@mynameistito/opencode-doomscroll", + "version": "0.1.0", + "dependencies": { + "@opencode/plugin": "catalog:", + }, + "devDependencies": { + "@types/node": "catalog:", + "effect": "4.0.1", + "solid-js": "catalog:", + "typescript": "catalog:", + "vite": "^8.0.0", + "vite-plugin-solid": "2.11.14", + }, + }, "packages/opencode-force-input": { "name": "@mynameistito/opencode-force-input", "version": "1.1.20", @@ -590,6 +605,8 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.32.2", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-SWsVtH/0vULSQStoZ6xxH2K09Li8fXYTMC64ZeogCdpZ3XSi1eCPOByV1OpuJxuI3OcmHhosDWYyii07tpPsLw=="], + "@mynameistito/opencode-doomscroll": ["@mynameistito/opencode-doomscroll@workspace:packages/opencode-doomscroll"], + "@mynameistito/opencode-force-input": ["@mynameistito/opencode-force-input@workspace:packages/opencode-force-input"], "@mynameistito/opencode-plugins-docs": ["@mynameistito/opencode-plugins-docs@workspace:apps/web"], @@ -1750,7 +1767,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + "effect": ["effect@4.0.1", "", {}, "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], @@ -3210,18 +3227,28 @@ "@ai-sdk/provider-utils/undici": ["undici@7.30.1", "", {}, "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg=="], + "@alchemy.run/acme/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + + "@alchemy.run/cloudflare/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + "@alchemy.run/cloudflare/rolldown": ["rolldown@1.2.13", "", { "dependencies": { "@oxc-project/types": "=0.153.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.13", "@rolldown/binding-android-arm64": "1.2.13", "@rolldown/binding-darwin-arm64": "1.2.13", "@rolldown/binding-darwin-x64": "1.2.13", "@rolldown/binding-freebsd-x64": "1.2.13", "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", "@rolldown/binding-linux-arm64-gnu": "1.2.13", "@rolldown/binding-linux-arm64-musl": "1.2.13", "@rolldown/binding-linux-ppc64-gnu": "1.2.13", "@rolldown/binding-linux-s390x-gnu": "1.2.13", "@rolldown/binding-linux-x64-gnu": "1.2.13", "@rolldown/binding-linux-x64-musl": "1.2.13", "@rolldown/binding-openharmony-arm64": "1.2.13", "@rolldown/binding-win32-arm64-msvc": "1.2.13", "@rolldown/binding-win32-x64-msvc": "1.2.13" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ=="], + "@alchemy.run/cloudflare-runtime/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + "@alchemy.run/cloudflare-runtime/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "@alchemy.run/cloudflare-runtime/mime": ["mime@4.1.1", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-YAApRsTpM2+gnUFHhRNGfkX8NK0V5xj1nyQLufGlwEI+eX4xRsTcel2IXDuMEGSGjBvzykNW4VuoEACgwYe7BQ=="], "@alchemy.run/cloudflare-runtime/rolldown": ["rolldown@1.2.14", "", { "dependencies": { "@oxc-project/types": "=0.154.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.14", "@rolldown/binding-android-arm64": "1.2.14", "@rolldown/binding-darwin-arm64": "1.2.14", "@rolldown/binding-darwin-x64": "1.2.14", "@rolldown/binding-freebsd-x64": "1.2.14", "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", "@rolldown/binding-linux-arm64-gnu": "1.2.14", "@rolldown/binding-linux-arm64-musl": "1.2.14", "@rolldown/binding-linux-ppc64-gnu": "1.2.14", "@rolldown/binding-linux-s390x-gnu": "1.2.14", "@rolldown/binding-linux-x64-gnu": "1.2.14", "@rolldown/binding-linux-x64-musl": "1.2.14", "@rolldown/binding-openharmony-arm64": "1.2.14", "@rolldown/binding-win32-arm64-msvc": "1.2.14", "@rolldown/binding-win32-x64-msvc": "1.2.14" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw=="], + "@alchemy.run/docker/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + "@alchemy.run/docker/rolldown": ["rolldown@1.2.13", "", { "dependencies": { "@oxc-project/types": "=0.153.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.13", "@rolldown/binding-android-arm64": "1.2.13", "@rolldown/binding-darwin-arm64": "1.2.13", "@rolldown/binding-darwin-x64": "1.2.13", "@rolldown/binding-freebsd-x64": "1.2.13", "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", "@rolldown/binding-linux-arm64-gnu": "1.2.13", "@rolldown/binding-linux-arm64-musl": "1.2.13", "@rolldown/binding-linux-ppc64-gnu": "1.2.13", "@rolldown/binding-linux-s390x-gnu": "1.2.13", "@rolldown/binding-linux-x64-gnu": "1.2.13", "@rolldown/binding-linux-x64-musl": "1.2.13", "@rolldown/binding-openharmony-arm64": "1.2.13", "@rolldown/binding-win32-arm64-msvc": "1.2.13", "@rolldown/binding-win32-x64-msvc": "1.2.13" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ=="], "@alchemy.run/node-utils/rolldown": ["rolldown@1.2.13", "", { "dependencies": { "@oxc-project/types": "=0.153.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.13", "@rolldown/binding-android-arm64": "1.2.13", "@rolldown/binding-darwin-arm64": "1.2.13", "@rolldown/binding-darwin-x64": "1.2.13", "@rolldown/binding-freebsd-x64": "1.2.13", "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", "@rolldown/binding-linux-arm64-gnu": "1.2.13", "@rolldown/binding-linux-arm64-musl": "1.2.13", "@rolldown/binding-linux-ppc64-gnu": "1.2.13", "@rolldown/binding-linux-s390x-gnu": "1.2.13", "@rolldown/binding-linux-x64-gnu": "1.2.13", "@rolldown/binding-linux-x64-musl": "1.2.13", "@rolldown/binding-openharmony-arm64": "1.2.13", "@rolldown/binding-win32-arm64-msvc": "1.2.13", "@rolldown/binding-win32-x64-msvc": "1.2.13" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ=="], + "@alchemy.run/sql/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + "@astrojs/astro2tsx/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" } }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], "@astrojs/astro2tsx/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="], @@ -3252,6 +3279,26 @@ "@bruits/satteri-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + "@distilled.cloud/acme/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + + "@distilled.cloud/aws4signer/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + + "@distilled.cloud/cloudflare/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + + "@distilled.cloud/core/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + + "@distilled.cloud/iceberg/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + + "@effect/opentelemetry/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + + "@effect/platform-bun/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + + "@effect/platform-node/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + + "@effect/platform-node-shared/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + + "@effect/sql-d1/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -3274,6 +3321,10 @@ "@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "@mynameistito/opencode-plugins-docs/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + + "@mynameistito/opencode-usage-limits/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + "@npmcli/agent/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], "@npmcli/arborist/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], @@ -3290,16 +3341,6 @@ "@npmcli/query/postcss-selector-parser": ["postcss-selector-parser@7.1.7", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g=="], - "@opencode/ai/effect": ["effect@4.0.1", "", {}, "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw=="], - - "@opencode/plugin/effect": ["effect@4.0.1", "", {}, "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw=="], - - "@opencode/protocol/effect": ["effect@4.0.1", "", {}, "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw=="], - - "@opencode/schema/effect": ["effect@4.0.1", "", {}, "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw=="], - - "@opencode/util/effect": ["effect@4.0.1", "", {}, "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw=="], - "@opencode/util/glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], "@opencode/util/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], @@ -3360,6 +3401,8 @@ "ajv-i18n/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "alchemy/effect": ["effect@4.0.6", "", {}, "sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw=="], + "alchemy/rolldown": ["rolldown@1.2.13", "", { "dependencies": { "@oxc-project/types": "=0.153.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.13", "@rolldown/binding-android-arm64": "1.2.13", "@rolldown/binding-darwin-arm64": "1.2.13", "@rolldown/binding-darwin-x64": "1.2.13", "@rolldown/binding-freebsd-x64": "1.2.13", "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", "@rolldown/binding-linux-arm64-gnu": "1.2.13", "@rolldown/binding-linux-arm64-musl": "1.2.13", "@rolldown/binding-linux-ppc64-gnu": "1.2.13", "@rolldown/binding-linux-s390x-gnu": "1.2.13", "@rolldown/binding-linux-x64-gnu": "1.2.13", "@rolldown/binding-linux-x64-musl": "1.2.13", "@rolldown/binding-openharmony-arm64": "1.2.13", "@rolldown/binding-win32-arm64-msvc": "1.2.13", "@rolldown/binding-win32-x64-msvc": "1.2.13" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], diff --git a/knip.json b/knip.json index 387ca52..f64c90c 100644 --- a/knip.json +++ b/knip.json @@ -16,6 +16,9 @@ }, "apps/web": { "entry": ["alchemy.run.ts", "blume.config.ts"] + }, + "packages/opencode-doomscroll": { + "entry": ["src/gui.tsx"] } } } diff --git a/packages/opencode-doomscroll/LICENSE b/packages/opencode-doomscroll/LICENSE new file mode 100644 index 0000000..5681c2a --- /dev/null +++ b/packages/opencode-doomscroll/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 mynameistito + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/opencode-doomscroll/README.md b/packages/opencode-doomscroll/README.md new file mode 100644 index 0000000..a55d046 --- /dev/null +++ b/packages/opencode-doomscroll/README.md @@ -0,0 +1,52 @@ +# @mynameistito/opencode-doomscroll + +An experimental OpenCode Desktop GUI plugin that opens X's home timeline in OpenCode's built-in browser when a session starts running. It closes the tab it opened when the run ends. OpenCode does not automate scrolling or interact with X. + +## Local development + +1. From the monorepo root, install dependencies and build the GUI bundle: + + ```powershell + bun install --frozen-lockfile + cd packages/opencode-doomscroll + bun run build + ``` + +2. Edit `%USERPROFILE%\.config\opencode\opencode.json` (or `.jsonc`) and add the package directory to its `plugins` list. Use an absolute path to this checkout, for example: + + ```json + { + "$schema": "https://opencode.ai/v2/opencode.json", + "plugins": [ + "C:/Users/mynameistito/code/opencode-plugins/packages/opencode-doomscroll" + ] + } + ``` + + Merge that plugin entry into your existing global config rather than replacing other settings. The Desktop app reads `opencode.json`, not `cli.json`. + +3. Restart OpenCode Desktop. Start a session; when it begins running, the plugin opens `https://x.com/home` in OpenCode's browser pane. The tab closes when the run succeeds, fails, or is interrupted. Sign in in the browser pane if X asks. + +For live development, leave this running while Desktop is open: + +```powershell +cd packages/opencode-doomscroll +bun run dev +``` + +Each rebuild reloads the GUI plugin. Pause automatic opening from the Doomscroll settings page; this switch applies to the current window. You can open X manually from the command palette with “Open X”. + +To keep automatic opening disabled after a restart, configure the plugin entry with an option: + +```json +{ + "plugins": [ + { + "package": "C:/Users/mynameistito/code/opencode-plugins/packages/opencode-doomscroll", + "options": { "autoOpen": false } + } + ] +} +``` + +The GUI plugin API is experimental in OpenCode v2. It requires OpenCode Desktop and its built-in Browser plugin. diff --git a/packages/opencode-doomscroll/package.json b/packages/opencode-doomscroll/package.json new file mode 100644 index 0000000..51f859b --- /dev/null +++ b/packages/opencode-doomscroll/package.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@mynameistito/opencode-doomscroll", + "version": "0.1.0", + "description": "Open X in OpenCode Desktop while a session is running.", + "keywords": [ + "ai-coding", + "desktop", + "opencode", + "opencode-plugin", + "twitter", + "x" + ], + "homepage": "https://github.com/mynameistito/opencode-plugins/tree/main/packages/opencode-doomscroll#readme", + "bugs": { + "url": "https://github.com/mynameistito/opencode-plugins/issues" + }, + "license": "MIT", + "author": { + "name": "Tito", + "email": "github@mynameistito.com", + "url": "https://github.com/mynameistito" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/mynameistito/opencode-plugins.git", + "directory": "packages/opencode-doomscroll" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "type": "module", + "exports": { + "./gui-experimental": "./dist/gui-experimental.cjs" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "scripts": { + "build": "vite build", + "dev": "vite build --watch", + "typecheck": "tsc --noEmit", + "prepack": "bun run build" + }, + "dependencies": { + "@opencode/plugin": "catalog:" + }, + "devDependencies": { + "@types/node": "catalog:", + "effect": "4.0.1", + "solid-js": "catalog:", + "typescript": "catalog:", + "vite": "^8.0.0", + "vite-plugin-solid": "2.11.14" + }, + "packageManager": "bun@1.4.2" +} diff --git a/packages/opencode-doomscroll/src/doomscroll-ipc.ts b/packages/opencode-doomscroll/src/doomscroll-ipc.ts new file mode 100644 index 0000000..720b729 --- /dev/null +++ b/packages/opencode-doomscroll/src/doomscroll-ipc.ts @@ -0,0 +1,131 @@ +import { Ipc } from "@opencode/plugin/gui"; +import { Schema } from "effect"; + +const BrowserTabSchema = Schema.Struct({ + canGoBack: Schema.Boolean, + canGoForward: Schema.Boolean, + generation: Schema.Number, + id: Schema.String, + loadError: Schema.optional(Schema.String), + loading: Schema.Boolean, + title: Schema.String, + url: Schema.String, +}); + +export type BrowserTab = typeof BrowserTabSchema.Type; + +const BrowserState = Schema.Struct({ + focusedTabID: Schema.NullOr(Schema.String), + tabs: Schema.Array(BrowserTabSchema), +}); + +const PaneElementSchema = Schema.Struct({ + label: Schema.String, + name: Schema.optional(Schema.String), + rect: Schema.Struct({ + height: Schema.Number, + width: Schema.Number, + x: Schema.Number, + y: Schema.Number, + }), + ref: Schema.String, + role: Schema.optional(Schema.String), + selector: Schema.String, + text: Schema.optional(Schema.String), +}); + +const PaneEvent = Schema.Union([ + Schema.Struct({ tabID: Schema.String, type: Schema.Literal("focus") }), + Schema.Struct({ + embed: Schema.String, + tabID: Schema.String, + type: Schema.Literal("embed"), + }), + Schema.Struct({ + active: Schema.Boolean, + element: Schema.optional(PaneElementSchema), + tabID: Schema.String, + type: Schema.Literal("inspect"), + }), + Schema.Struct({ + icon: Schema.optional(Schema.String), + tabID: Schema.String, + type: Schema.Literal("page"), + zoom: Schema.Number, + }), + Schema.Struct({ path: Schema.String, type: Schema.Literal("preview") }), + Schema.Struct({ + alt: Schema.Boolean, + code: Schema.String, + ctrl: Schema.Boolean, + key: Schema.String, + meta: Schema.Boolean, + shift: Schema.Boolean, + type: Schema.Literal("shortcut"), + }), + Schema.Struct({ tabID: Schema.String, type: Schema.Literal("focused") }), + Schema.Struct({ + error: Schema.optional(Schema.String), + state: Schema.NullOr(BrowserState), + type: Schema.Literal("state"), + }), +]); + +const BrowserPaneEventSchema = Schema.Struct({ + binding: Schema.String, + event: PaneEvent, +}); + +export type BrowserPaneEvent = typeof BrowserPaneEventSchema.Type; + +const binding = Schema.String; +const tabID = Schema.String; +const BrowserAction = Schema.Unknown; + +// This mirrors the built-in Browser pane IPC. Keep the full method surface here: +// both plugins resolve the same IPC ID, and the Browser plugin needs register() +// before it can attach a session's pane. +export const BrowserPane = Ipc.define({ + events: { event: BrowserPaneEventSchema }, + id: "browser.pane", + methods: { + clearSite: { input: Schema.Struct({ binding, tabID }) }, + close: { input: Schema.Struct({ binding }) }, + command: { + input: Schema.Struct({ + binding, + command: BrowserAction, + }), + }, + highlight: { + input: Schema.Struct({ + binding, + ref: Schema.optional(Schema.String), + tabID, + }), + }, + inspect: { + input: Schema.Struct({ binding, enabled: Schema.Boolean, tabID }), + }, + load: { input: Schema.Struct({ binding, tabID }) }, + register: { + input: Schema.Struct({ + binding, + restore: Schema.optional(BrowserState), + server: Schema.String, + session: Schema.String, + }), + }, + site: { + input: Schema.Struct({ binding, tabID }), + output: Schema.Struct({ cookies: Schema.Int }), + }, + zoom: { + input: Schema.Struct({ + binding, + tabID, + zoom: Schema.Literals(["in", "out", "reset"]), + }), + }, + }, +}); diff --git a/packages/opencode-doomscroll/src/gui.tsx b/packages/opencode-doomscroll/src/gui.tsx new file mode 100644 index 0000000..ebbbe3b --- /dev/null +++ b/packages/opencode-doomscroll/src/gui.tsx @@ -0,0 +1,383 @@ +import { + Command, + Plugin, + SettingsPage, + createKeyed, +} from "@opencode/plugin/gui"; +import { Browser } from "@opencode/plugin/gui/browser"; +import { createSignal } from "solid-js"; + +import { BrowserPane } from "./doomscroll-ipc"; +import type { BrowserPaneEvent, BrowserTab } from "./doomscroll-ipc"; + +const X_HOME_URL = "https://x.com/home"; +const OPEN_X_KEY = "command.openX"; +const CLOSE_FAILED_KEY = "toast.closeFailed"; +const OPEN_FAILED_KEY = "toast.openFailed"; +const TOAST_ERROR = "error" as const; +const START_EVENTS = ["session.execution.started"] as const; + +const STOP_EVENTS = [ + "session.execution.succeeded", + "session.execution.failed", + "session.execution.interrupted", +] as const; + +interface DoomscrollTab { + readonly binding: string; + readonly tabID: string; +} + +interface PendingOpen { + closeRequested: boolean; +} + +const matchesTab = ( + tab: BrowserTab, + previous: ReadonlyMap | undefined, + focusedTabID: string | null, + allowFocusFallback: boolean +): boolean => + allowFocusFallback && + previous !== undefined && + !previous.has(tab.id) && + tab.id === focusedTabID; + +export default Plugin.define({ + i18n: { + en: { + "command.openX": "Open X", + "settings.autoOpen": "Open X when a session starts", + "settings.autoOpenDescription": + "Pause or resume automatic X opening for this window.", + "settings.description": + "X opens in OpenCode's browser and the tab closes when the session run ends.", + "settings.title": "Doomscroll", + "toast.browserUnavailable": + "The built-in Browser plugin is off or unavailable. Enable it in Extensions, then try again.", + "toast.closeFailed": "Could not close the Doomscroll tab", + "toast.closeUnconfirmed": + "OpenCode accepted the close request, but the browser still reports this tab as open.", + "toast.openFailed": "Could not open the Doomscroll tab", + }, + }, + os: ["macos", "windows", "linux"], + setup(ctx) { + const [autoOpen, setAutoOpen] = createSignal( + ctx.options.autoOpen !== false + ); + + const browser = ctx.use(Browser); + const browserPane = ctx.use(BrowserPane); + const pending = new Map(); + const tabs = new Map(); + const states = new Map>(); + + const findSession = (sessionID: string) => + ctx.sessions + .list() + .find( + (session) => + session.id === sessionID && session.server.id === "sidecar" + ); + + const findTopLevelSession = async (sessionID: string) => { + const session = findSession(sessionID); + + if (!session) { + return null; + } + + try { + const info = await session.server.client.session.get({ sessionID }); + + if (info.parentID && !info.fork) { + return null; + } + } catch { + // The desktop's shell session reference is the fallback if the server + // cannot provide session metadata during a connection transition. + } + + return session; + }; + + const queuedOpen = new Map< + string, + NonNullable> + >(); + + const closeTab = async ( + sessionKey: string, + doomscrollTab: DoomscrollTab + ): Promise => { + const live = browserPane(); + + if (live.status !== "active") { + tabs.set(sessionKey, doomscrollTab); + ctx.toast.show({ + description: + "OpenCode's browser connection is unavailable, so the X tab may still be open.", + title: ctx.t(CLOSE_FAILED_KEY), + variant: TOAST_ERROR, + }); + + return; + } + + try { + await live.value.command({ + binding: doomscrollTab.binding, + command: { tabID: doomscrollTab.tabID, type: "tabs.close" }, + }); + setTimeout(() => { + if (tabs.get(sessionKey) !== doomscrollTab) { + return; + } + + ctx.toast.show({ + description: ctx.t("toast.closeUnconfirmed"), + title: ctx.t(CLOSE_FAILED_KEY), + variant: TOAST_ERROR, + }); + }, 1500); + } catch (error: unknown) { + const failure = + error instanceof Error ? error : new Error(String(error)); + + ctx.toast.show({ + description: failure.message, + title: ctx.t(CLOSE_FAILED_KEY), + variant: TOAST_ERROR, + }); + } + }; + + const onBrowserEvent = (value: BrowserPaneEvent): void => { + const { binding, event } = value; + + if (event.type !== "state") { + return; + } + + const { state } = event; + const browserTabs = state?.tabs ?? []; + const previous = states.get(binding); + + if (state) { + states.set( + binding, + new Map(browserTabs.map((tab) => [tab.id, tab.url])) + ); + } else { + states.delete(binding); + } + + for (const [sessionKey, doomscrollTab] of tabs) { + if (doomscrollTab.binding !== binding) { + continue; + } + + if (!browserTabs.some((tab) => tab.id === doomscrollTab.tabID)) { + tabs.delete(sessionKey); + } + } + + for (const [sessionKey, request] of pending) { + const opened = browserTabs.find((tab) => + matchesTab( + tab, + previous, + state?.focusedTabID ?? null, + pending.size === 1 + ) + ); + + if (opened) { + pending.delete(sessionKey); + const doomscrollTab = { binding, tabID: opened.id }; + tabs.set(sessionKey, doomscrollTab); + + if (request.closeRequested) { + void closeTab(sessionKey, doomscrollTab); + } + + return; + } + } + }; + + const openX = ( + session: NonNullable> + ): void => { + const request = pending.get(session.key); + + if (request) { + request.closeRequested = false; + + return; + } + + if (queuedOpen.has(session.key) || tabs.has(session.key)) { + return; + } + + const live = browser(); + + if (live.status === "pending") { + queuedOpen.set(session.key, session); + + return; + } + + if (live.status !== "active") { + ctx.toast.show({ + description: ctx.t("toast.browserUnavailable"), + title: ctx.t(OPEN_FAILED_KEY), + variant: TOAST_ERROR, + }); + + return; + } + + queuedOpen.delete(session.key); + const openRequest: PendingOpen = { closeRequested: false }; + pending.set(session.key, openRequest); + setTimeout(() => { + if (pending.get(session.key) === openRequest) { + pending.delete(session.key); + } + }, 10_000); + + try { + live.value.open(session, X_HOME_URL); + } catch (error: unknown) { + pending.delete(session.key); + + const failure = + error instanceof Error ? error : new Error(String(error)); + + ctx.toast.show({ + description: failure.message, + title: ctx.t(OPEN_FAILED_KEY), + variant: TOAST_ERROR, + }); + } + }; + + const closeX = ( + session: NonNullable> + ): void => { + const request = pending.get(session.key); + queuedOpen.delete(session.key); + + if (request) { + request.closeRequested = true; + } + + const doomscrollTab = tabs.get(session.key); + + if (doomscrollTab) { + void closeTab(session.key, doomscrollTab); + } + }; + + ctx.add(Command, (): Command => ({ + enabled: ctx.app.platform === "desktop" && !!ctx.sessions.current(), + id: "open-x", + keywords: "x twitter doomscroll break", + run: () => { + const session = ctx.sessions.current(); + + if (session) { + openX(session); + } + }, + section: "session", + title: ctx.t(OPEN_X_KEY), + })); + + ctx.add(SettingsPage, { + icon: "globe", + id: "preferences", + render: () => ( +
+ +

{ctx.t("settings.description")}

+
+ ), + get title() { + return ctx.t("settings.title"); + }, + }); + + createKeyed(browserPane, (live) => live.on("event", onBrowserEvent)); + + createKeyed( + browser, + () => { + const sessions = [...queuedOpen.values()]; + queuedOpen.clear(); + + for (const session of sessions) { + openX(session); + } + }, + { + otherwise: () => { + if (browser().status !== "inactive" || queuedOpen.size === 0) { + return; + } + + ctx.toast.show({ + description: ctx.t("toast.browserUnavailable"), + title: ctx.t(OPEN_FAILED_KEY), + variant: TOAST_ERROR, + }); + queuedOpen.clear(); + }, + } + ); + + createKeyed( + () => ctx.servers.get("sidecar")?.data, + (data) => { + const subscriptions = [ + ...START_EVENTS.map((type) => + data.on(type, async (event) => { + if (!autoOpen()) { + return; + } + + const session = await findTopLevelSession(event.data.sessionID); + + if (session) { + openX(session); + } + }) + ), + ...STOP_EVENTS.map((type) => + data.on(type, async (event) => { + const session = await findTopLevelSession(event.data.sessionID); + + if (session) { + closeX(session); + } + }) + ), + ]; + + return () => { + for (const unsubscribe of subscriptions) { + unsubscribe(); + } + }; + } + ); + }, +}); diff --git a/packages/opencode-doomscroll/tsconfig.json b/packages/opencode-doomscroll/tsconfig.json new file mode 100644 index 0000000..6c20246 --- /dev/null +++ b/packages/opencode-doomscroll/tsconfig.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://www.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "lib": ["ESNext", "DOM"], + "types": ["node"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"] +} diff --git a/packages/opencode-doomscroll/vite.config.ts b/packages/opencode-doomscroll/vite.config.ts new file mode 100644 index 0000000..2874bdc --- /dev/null +++ b/packages/opencode-doomscroll/vite.config.ts @@ -0,0 +1,4 @@ +import { gui } from "@opencode/plugin/gui/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ plugins: [gui()] }); diff --git a/scripts/changeset-add.ts b/scripts/changeset-add.ts index 4d4fb39..b25959a 100644 --- a/scripts/changeset-add.ts +++ b/scripts/changeset-add.ts @@ -5,6 +5,7 @@ import path from "node:path"; const packages = new Map([ ["docs", "@mynameistito/opencode-plugins-docs"], ["force-input", "@mynameistito/opencode-force-input"], + ["doomscroll", "@mynameistito/opencode-doomscroll"], ["usage-limits", "@mynameistito/opencode-usage-limits"], ]); @@ -18,7 +19,7 @@ const summary = summaryParts.join(" ").trim(); if (!packageName || !type || !types.has(type) || !summary) { console.error( - 'Usage: bun run changeset-add "summary"' + 'Usage: bun run changeset-add "summary"' ); process.exit(1); }