From d72d840d9fe576db1ebed53846bfa179eda34d8f Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:01:20 +1300 Subject: [PATCH 01/14] feat(doomscroll): add Instagram Reels GUI plugin --- .changeset/40f76543.md | 5 + bun.lock | 65 +++- packages/opencode-doomscroll/LICENSE | 21 ++ packages/opencode-doomscroll/README.md | 52 +++ packages/opencode-doomscroll/package.json | 60 ++++ packages/opencode-doomscroll/src/gui.tsx | 326 ++++++++++++++++++ packages/opencode-doomscroll/src/reels-ipc.ts | 97 ++++++ packages/opencode-doomscroll/tsconfig.json | 11 + packages/opencode-doomscroll/vite.config.ts | 4 + scripts/changeset-add.ts | 3 +- 10 files changed, 632 insertions(+), 12 deletions(-) create mode 100644 .changeset/40f76543.md create mode 100644 packages/opencode-doomscroll/LICENSE create mode 100644 packages/opencode-doomscroll/README.md create mode 100644 packages/opencode-doomscroll/package.json create mode 100644 packages/opencode-doomscroll/src/gui.tsx create mode 100644 packages/opencode-doomscroll/src/reels-ipc.ts create mode 100644 packages/opencode-doomscroll/tsconfig.json create mode 100644 packages/opencode-doomscroll/vite.config.ts diff --git a/.changeset/40f76543.md b/.changeset/40f76543.md new file mode 100644 index 0000000..1afd1ca --- /dev/null +++ b/.changeset/40f76543.md @@ -0,0 +1,5 @@ +--- +"@mynameistito/opencode-doomscroll": minor +--- + +Add an experimental GUI plugin that opens Instagram Reels 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/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..39e0445 --- /dev/null +++ b/packages/opencode-doomscroll/README.md @@ -0,0 +1,52 @@ +# @mynameistito/opencode-doomscroll + +An experimental OpenCode Desktop GUI plugin that opens Instagram Reels 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 Instagram. + +## 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://www.instagram.com/reels/` in OpenCode's browser pane. The tab closes when the run succeeds, fails, or is interrupted. Sign in in the browser pane if Instagram 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 Reels manually from the command palette with “Open Instagram Reels”. + +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..4af277c --- /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 Instagram Reels in OpenCode Desktop while a session is running.", + "keywords": [ + "ai-coding", + "desktop", + "instagram", + "opencode", + "opencode-plugin", + "reels" + ], + "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/gui.tsx b/packages/opencode-doomscroll/src/gui.tsx new file mode 100644 index 0000000..a8ae561 --- /dev/null +++ b/packages/opencode-doomscroll/src/gui.tsx @@ -0,0 +1,326 @@ +import { + Command, + Plugin, + SettingsPage, + createKeyed, +} from "@opencode/plugin/gui"; +import { Browser } from "@opencode/plugin/gui/browser"; +import { createSignal } from "solid-js"; + +import { BrowserPane } from "./reels-ipc"; +import type { BrowserPaneEvent, BrowserTab } from "./reels-ipc"; + +const REELS_URL = "https://www.instagram.com/reels/"; +const OPEN_REELS_KEY = "command.openReels"; +const CLOSE_FAILED_KEY = "toast.closeFailed"; +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 ReelsTab { + readonly binding: string; + readonly tabID: string; +} + +interface PendingOpen { + candidateTabID?: string; + readonly marker: string; + closeRequested: boolean; +} + +const matchesTab = ( + tab: BrowserTab, + previous: ReadonlyMap | undefined, + request: PendingOpen, + focusedTabID: string | null +): boolean => { + if (request.candidateTabID === tab.id) { + return true; + } + + const isNewTab = previous !== undefined && !previous.has(tab.id); + const hasMarker = tab.url.includes(request.marker); + + const isFocusedInstagramTab = + tab.id === focusedTabID && + /^https:\/\/(?:[\w-]+\.)*instagram\.com\//iu.test(tab.url); + // Browser.open can first publish a blank/loading tab, then update its URL in a + // later state event. Remember that tab ID now so the later state can confirm it. + + const isFocusedBlankTab = + tab.id === focusedTabID && (tab.url === "about:blank" || tab.url === ""); + + if (!hasMarker && !isNewTab) { + return false; + } + + if (hasMarker || isFocusedInstagramTab || isFocusedBlankTab) { + request.candidateTabID = tab.id; + + return true; + } + + return false; +}; + +export default Plugin.define({ + i18n: { + en: { + "command.openReels": "Open Instagram Reels", + "settings.autoOpen": "Open Reels when a session starts", + "settings.autoOpenDescription": + "Pause or resume automatic Reels opening for this window.", + "settings.description": + "Reels open in OpenCode's browser and the tab closes when the session run ends.", + "settings.title": "Doomscroll", + "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 closeTab = async ( + sessionKey: string, + reelsTab: ReelsTab + ): Promise => { + const live = browserPane(); + + if (live.status !== "active") { + tabs.set(sessionKey, reelsTab); + ctx.toast.show({ + description: + "OpenCode's browser connection is unavailable, so the Reels tab may still be open.", + title: ctx.t(CLOSE_FAILED_KEY), + variant: TOAST_ERROR, + }); + + return; + } + + try { + await live.value.command({ + binding: reelsTab.binding, + command: { tabID: reelsTab.tabID, type: "tabs.close" }, + }); + setTimeout(() => { + if (tabs.get(sessionKey) !== reelsTab) { + 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; + + if (!state) { + return; + } + + const { tabs: browserTabs } = state; + const previous = states.get(binding); + states.set(binding, new Map(browserTabs.map((tab) => [tab.id, tab.url]))); + + for (const [sessionKey, reelsTab] of tabs) { + if (reelsTab.binding !== binding) { + continue; + } + + if (!browserTabs.some((tab) => tab.id === reelsTab.tabID)) { + tabs.delete(sessionKey); + } + } + + for (const [sessionKey, request] of pending) { + const opened = browserTabs.find((tab) => + matchesTab(tab, previous, request, state.focusedTabID) + ); + + if (opened) { + pending.delete(sessionKey); + const reelsTab = { binding, tabID: opened.id }; + tabs.set(sessionKey, reelsTab); + + if (request.closeRequested) { + void closeTab(sessionKey, reelsTab); + } + + return; + } + } + }; + + const openReels = ( + session: NonNullable> + ): void => { + const live = browser(); + + if (live.status !== "active") { + ctx.toast.show({ + description: + "OpenCode's browser is unavailable for this session. Open the session in Desktop and try again.", + title: ctx.t("toast.openFailed"), + variant: TOAST_ERROR, + }); + + return; + } + + if (pending.has(session.key) || tabs.has(session.key)) { + return; + } + + const marker = `__doomscroll=${crypto.randomUUID()}`; + pending.set(session.key, { closeRequested: false, marker }); + + try { + live.value.open(session, `${REELS_URL}?${marker}`); + } 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("toast.openFailed"), + variant: TOAST_ERROR, + }); + } + }; + + const closeReels = ( + session: NonNullable> + ): void => { + const request = pending.get(session.key); + + if (request) { + request.closeRequested = true; + } + + const reelsTab = tabs.get(session.key); + + if (reelsTab) { + void closeTab(session.key, reelsTab); + } + }; + + ctx.add(Command, (): Command => ({ + enabled: ctx.app.platform === "desktop" && !!ctx.sessions.current(), + id: "open-reels", + keywords: "instagram reels doomscroll break", + run: () => { + const session = ctx.sessions.current(); + + if (session) { + openReels(session); + } + }, + section: "session", + title: ctx.t(OPEN_REELS_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( + () => ctx.servers.get("sidecar")?.data, + (data) => { + const subscriptions = [ + ...START_EVENTS.map((type) => + data.on(type, (event) => { + if (!autoOpen()) { + return; + } + + const session = findSession(event.data.sessionID); + + if (session) { + openReels(session); + } + }) + ), + ...STOP_EVENTS.map((type) => + data.on(type, (event) => { + const session = findSession(event.data.sessionID); + + if (session) { + closeReels(session); + } + }) + ), + ]; + + return () => { + for (const unsubscribe of subscriptions) { + unsubscribe(); + } + }; + } + ); + }, +}); diff --git a/packages/opencode-doomscroll/src/reels-ipc.ts b/packages/opencode-doomscroll/src/reels-ipc.ts new file mode 100644 index 0000000..7c8da21 --- /dev/null +++ b/packages/opencode-doomscroll/src/reels-ipc.ts @@ -0,0 +1,97 @@ +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; + +// The built-in Desktop browser pane accepts the same browser action used by +// browser.tabs.close. Keeping the Ipc narrow avoids duplicating its full protocol. +export const BrowserPane = Ipc.define({ + events: { event: BrowserPaneEventSchema }, + id: "browser.pane", + methods: { + command: { + input: Schema.Struct({ + binding: Schema.String, + command: Schema.Struct({ + tabID: Schema.String, + type: Schema.Literal("tabs.close"), + }), + }), + }, + }, +}); 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); } From 64d76571777ff5e0449ad0e0e01a4b64ec6b0785 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:03:31 +1300 Subject: [PATCH 02/14] fix(doomscroll): track tab independently of URL --- packages/opencode-doomscroll/src/gui.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/opencode-doomscroll/src/gui.tsx b/packages/opencode-doomscroll/src/gui.tsx index a8ae561..93636c5 100644 --- a/packages/opencode-doomscroll/src/gui.tsx +++ b/packages/opencode-doomscroll/src/gui.tsx @@ -45,21 +45,15 @@ const matchesTab = ( const isNewTab = previous !== undefined && !previous.has(tab.id); const hasMarker = tab.url.includes(request.marker); - - const isFocusedInstagramTab = - tab.id === focusedTabID && - /^https:\/\/(?:[\w-]+\.)*instagram\.com\//iu.test(tab.url); - // Browser.open can first publish a blank/loading tab, then update its URL in a - // later state event. Remember that tab ID now so the later state can confirm it. - - const isFocusedBlankTab = - tab.id === focusedTabID && (tab.url === "about:blank" || tab.url === ""); + // Browser.open selects the new tab. Match its ID even if the first state is + // blank or a redirect has already changed its URL. + const isNewFocusedTab = isNewTab && tab.id === focusedTabID; if (!hasMarker && !isNewTab) { return false; } - if (hasMarker || isFocusedInstagramTab || isFocusedBlankTab) { + if (hasMarker || isNewFocusedTab) { request.candidateTabID = tab.id; return true; From ad04dc75d86084265d997e0f5a89850a7488f440 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:07:42 +1300 Subject: [PATCH 03/14] fix(doomscroll): wait for browser provider --- packages/opencode-doomscroll/src/gui.tsx | 61 ++++++++++++++++++++---- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/packages/opencode-doomscroll/src/gui.tsx b/packages/opencode-doomscroll/src/gui.tsx index 93636c5..c4b1f57 100644 --- a/packages/opencode-doomscroll/src/gui.tsx +++ b/packages/opencode-doomscroll/src/gui.tsx @@ -13,6 +13,7 @@ import type { BrowserPaneEvent, BrowserTab } from "./reels-ipc"; const REELS_URL = "https://www.instagram.com/reels/"; const OPEN_REELS_KEY = "command.openReels"; 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; @@ -72,6 +73,8 @@ export default Plugin.define({ "settings.description": "Reels open 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.", @@ -98,6 +101,11 @@ export default Plugin.define({ session.id === sessionID && session.server.id === "sidecar" ); + const queuedOpen = new Map< + string, + NonNullable> + >(); + const closeTab = async ( sessionKey: string, reelsTab: ReelsTab @@ -193,24 +201,34 @@ export default Plugin.define({ const openReels = ( session: NonNullable> ): void => { + if ( + queuedOpen.has(session.key) || + pending.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: - "OpenCode's browser is unavailable for this session. Open the session in Desktop and try again.", - title: ctx.t("toast.openFailed"), + description: ctx.t("toast.browserUnavailable"), + title: ctx.t(OPEN_FAILED_KEY), variant: TOAST_ERROR, }); return; } - if (pending.has(session.key) || tabs.has(session.key)) { - return; - } - const marker = `__doomscroll=${crypto.randomUUID()}`; + queuedOpen.delete(session.key); pending.set(session.key, { closeRequested: false, marker }); try { @@ -223,7 +241,7 @@ export default Plugin.define({ ctx.toast.show({ description: failure.message, - title: ctx.t("toast.openFailed"), + title: ctx.t(OPEN_FAILED_KEY), variant: TOAST_ERROR, }); } @@ -233,6 +251,7 @@ export default Plugin.define({ session: NonNullable> ): void => { const request = pending.get(session.key); + queuedOpen.delete(session.key); if (request) { request.closeRequested = true; @@ -281,6 +300,32 @@ export default Plugin.define({ createKeyed(browserPane, (live) => live.on("event", onBrowserEvent)); + createKeyed( + browser, + () => { + const sessions = [...queuedOpen.values()]; + queuedOpen.clear(); + + for (const session of sessions) { + openReels(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) => { From 4fd2c773741ac7abe7b7e4be64867eedfe24b444 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:10:55 +1300 Subject: [PATCH 04/14] fix(doomscroll): preserve browser pane IPC methods --- packages/opencode-doomscroll/src/reels-ipc.ts | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/packages/opencode-doomscroll/src/reels-ipc.ts b/packages/opencode-doomscroll/src/reels-ipc.ts index 7c8da21..720b729 100644 --- a/packages/opencode-doomscroll/src/reels-ipc.ts +++ b/packages/opencode-doomscroll/src/reels-ipc.ts @@ -78,19 +78,53 @@ const BrowserPaneEventSchema = Schema.Struct({ export type BrowserPaneEvent = typeof BrowserPaneEventSchema.Type; -// The built-in Desktop browser pane accepts the same browser action used by -// browser.tabs.close. Keeping the Ipc narrow avoids duplicating its full protocol. +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: Schema.String, - command: Schema.Struct({ - tabID: Schema.String, - type: Schema.Literal("tabs.close"), - }), + 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"]), }), }, }, From ee34336515d277c40de2a73c02afadbaf9ed0158 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:13:25 +1300 Subject: [PATCH 05/14] fix(doomscroll): open clean reels URL --- packages/opencode-doomscroll/src/gui.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/opencode-doomscroll/src/gui.tsx b/packages/opencode-doomscroll/src/gui.tsx index c4b1f57..fbcd8c4 100644 --- a/packages/opencode-doomscroll/src/gui.tsx +++ b/packages/opencode-doomscroll/src/gui.tsx @@ -30,7 +30,6 @@ interface ReelsTab { interface PendingOpen { candidateTabID?: string; - readonly marker: string; closeRequested: boolean; } @@ -45,16 +44,11 @@ const matchesTab = ( } const isNewTab = previous !== undefined && !previous.has(tab.id); - const hasMarker = tab.url.includes(request.marker); // Browser.open selects the new tab. Match its ID even if the first state is // blank or a redirect has already changed its URL. const isNewFocusedTab = isNewTab && tab.id === focusedTabID; - if (!hasMarker && !isNewTab) { - return false; - } - - if (hasMarker || isNewFocusedTab) { + if (isNewFocusedTab) { request.candidateTabID = tab.id; return true; @@ -227,12 +221,11 @@ export default Plugin.define({ return; } - const marker = `__doomscroll=${crypto.randomUUID()}`; queuedOpen.delete(session.key); - pending.set(session.key, { closeRequested: false, marker }); + pending.set(session.key, { closeRequested: false }); try { - live.value.open(session, `${REELS_URL}?${marker}`); + live.value.open(session, REELS_URL); } catch (error: unknown) { pending.delete(session.key); From 3c20bdcf0aae735a4122bcc43ac0a19c79dbd049 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:19:24 +1300 Subject: [PATCH 06/14] fix(doomscroll): ignore subagent lifecycle events --- packages/opencode-doomscroll/src/gui.tsx | 29 ++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/opencode-doomscroll/src/gui.tsx b/packages/opencode-doomscroll/src/gui.tsx index fbcd8c4..e78cc5f 100644 --- a/packages/opencode-doomscroll/src/gui.tsx +++ b/packages/opencode-doomscroll/src/gui.tsx @@ -95,6 +95,27 @@ export default Plugin.define({ session.id === sessionID && session.server.id === "sidecar" ); + const findTopLevelSession = async (sessionID: string) => { + const session = findSession(sessionID); + + if (!session) { + return; + } + + try { + const info = await session.server.client.session.get({ sessionID }); + + if (info.parentID && !info.fork) { + return; + } + } 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> @@ -324,12 +345,12 @@ export default Plugin.define({ (data) => { const subscriptions = [ ...START_EVENTS.map((type) => - data.on(type, (event) => { + data.on(type, async (event) => { if (!autoOpen()) { return; } - const session = findSession(event.data.sessionID); + const session = await findTopLevelSession(event.data.sessionID); if (session) { openReels(session); @@ -337,8 +358,8 @@ export default Plugin.define({ }) ), ...STOP_EVENTS.map((type) => - data.on(type, (event) => { - const session = findSession(event.data.sessionID); + data.on(type, async (event) => { + const session = await findTopLevelSession(event.data.sessionID); if (session) { closeReels(session); From 9781d3b00fdbc4d4ea2a0447551d14f50ff2496f Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:27:14 +1300 Subject: [PATCH 07/14] feat(doomscroll): switch feed from Instagram to X --- .../opencode-doomscroll/src/{reels-ipc.ts => doomscroll-ipc.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/opencode-doomscroll/src/{reels-ipc.ts => doomscroll-ipc.ts} (100%) diff --git a/packages/opencode-doomscroll/src/reels-ipc.ts b/packages/opencode-doomscroll/src/doomscroll-ipc.ts similarity index 100% rename from packages/opencode-doomscroll/src/reels-ipc.ts rename to packages/opencode-doomscroll/src/doomscroll-ipc.ts From 28090fd13644aeceea9e796263e83bb88fd8eb2c Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:27:24 +1300 Subject: [PATCH 08/14] feat(doomscroll): switch feed from Instagram to X --- packages/opencode-doomscroll/README.md | 6 +- packages/opencode-doomscroll/package.json | 6 +- packages/opencode-doomscroll/src/gui.tsx | 70 +++++++++++------------ 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/opencode-doomscroll/README.md b/packages/opencode-doomscroll/README.md index 39e0445..a55d046 100644 --- a/packages/opencode-doomscroll/README.md +++ b/packages/opencode-doomscroll/README.md @@ -1,6 +1,6 @@ # @mynameistito/opencode-doomscroll -An experimental OpenCode Desktop GUI plugin that opens Instagram Reels 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 Instagram. +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 @@ -25,7 +25,7 @@ An experimental OpenCode Desktop GUI plugin that opens Instagram Reels in OpenCo 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://www.instagram.com/reels/` in OpenCode's browser pane. The tab closes when the run succeeds, fails, or is interrupted. Sign in in the browser pane if Instagram asks. +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: @@ -34,7 +34,7 @@ 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 Reels manually from the command palette with “Open Instagram Reels”. +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: diff --git a/packages/opencode-doomscroll/package.json b/packages/opencode-doomscroll/package.json index 4af277c..51f859b 100644 --- a/packages/opencode-doomscroll/package.json +++ b/packages/opencode-doomscroll/package.json @@ -2,14 +2,14 @@ "$schema": "https://json.schemastore.org/package.json", "name": "@mynameistito/opencode-doomscroll", "version": "0.1.0", - "description": "Open Instagram Reels in OpenCode Desktop while a session is running.", + "description": "Open X in OpenCode Desktop while a session is running.", "keywords": [ "ai-coding", "desktop", - "instagram", "opencode", "opencode-plugin", - "reels" + "twitter", + "x" ], "homepage": "https://github.com/mynameistito/opencode-plugins/tree/main/packages/opencode-doomscroll#readme", "bugs": { diff --git a/packages/opencode-doomscroll/src/gui.tsx b/packages/opencode-doomscroll/src/gui.tsx index e78cc5f..486a338 100644 --- a/packages/opencode-doomscroll/src/gui.tsx +++ b/packages/opencode-doomscroll/src/gui.tsx @@ -7,11 +7,11 @@ import { import { Browser } from "@opencode/plugin/gui/browser"; import { createSignal } from "solid-js"; -import { BrowserPane } from "./reels-ipc"; -import type { BrowserPaneEvent, BrowserTab } from "./reels-ipc"; +import { BrowserPane } from "./doomscroll-ipc"; +import type { BrowserPaneEvent, BrowserTab } from "./doomscroll-ipc"; -const REELS_URL = "https://www.instagram.com/reels/"; -const OPEN_REELS_KEY = "command.openReels"; +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; @@ -23,7 +23,7 @@ const STOP_EVENTS = [ "session.execution.interrupted", ] as const; -interface ReelsTab { +interface DoomscrollTab { readonly binding: string; readonly tabID: string; } @@ -60,12 +60,12 @@ const matchesTab = ( export default Plugin.define({ i18n: { en: { - "command.openReels": "Open Instagram Reels", - "settings.autoOpen": "Open Reels when a session starts", + "command.openX": "Open X", + "settings.autoOpen": "Open X when a session starts", "settings.autoOpenDescription": - "Pause or resume automatic Reels opening for this window.", + "Pause or resume automatic X opening for this window.", "settings.description": - "Reels open in OpenCode's browser and the tab closes when the session run ends.", + "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.", @@ -84,7 +84,7 @@ export default Plugin.define({ const browser = ctx.use(Browser); const browserPane = ctx.use(BrowserPane); const pending = new Map(); - const tabs = new Map(); + const tabs = new Map(); const states = new Map>(); const findSession = (sessionID: string) => @@ -123,15 +123,15 @@ export default Plugin.define({ const closeTab = async ( sessionKey: string, - reelsTab: ReelsTab + doomscrollTab: DoomscrollTab ): Promise => { const live = browserPane(); if (live.status !== "active") { - tabs.set(sessionKey, reelsTab); + tabs.set(sessionKey, doomscrollTab); ctx.toast.show({ description: - "OpenCode's browser connection is unavailable, so the Reels tab may still be open.", + "OpenCode's browser connection is unavailable, so the X tab may still be open.", title: ctx.t(CLOSE_FAILED_KEY), variant: TOAST_ERROR, }); @@ -141,11 +141,11 @@ export default Plugin.define({ try { await live.value.command({ - binding: reelsTab.binding, - command: { tabID: reelsTab.tabID, type: "tabs.close" }, + binding: doomscrollTab.binding, + command: { tabID: doomscrollTab.tabID, type: "tabs.close" }, }); setTimeout(() => { - if (tabs.get(sessionKey) !== reelsTab) { + if (tabs.get(sessionKey) !== doomscrollTab) { return; } @@ -184,12 +184,12 @@ export default Plugin.define({ const previous = states.get(binding); states.set(binding, new Map(browserTabs.map((tab) => [tab.id, tab.url]))); - for (const [sessionKey, reelsTab] of tabs) { - if (reelsTab.binding !== binding) { + for (const [sessionKey, doomscrollTab] of tabs) { + if (doomscrollTab.binding !== binding) { continue; } - if (!browserTabs.some((tab) => tab.id === reelsTab.tabID)) { + if (!browserTabs.some((tab) => tab.id === doomscrollTab.tabID)) { tabs.delete(sessionKey); } } @@ -201,11 +201,11 @@ export default Plugin.define({ if (opened) { pending.delete(sessionKey); - const reelsTab = { binding, tabID: opened.id }; - tabs.set(sessionKey, reelsTab); + const doomscrollTab = { binding, tabID: opened.id }; + tabs.set(sessionKey, doomscrollTab); if (request.closeRequested) { - void closeTab(sessionKey, reelsTab); + void closeTab(sessionKey, doomscrollTab); } return; @@ -213,7 +213,7 @@ export default Plugin.define({ } }; - const openReels = ( + const openX = ( session: NonNullable> ): void => { if ( @@ -246,7 +246,7 @@ export default Plugin.define({ pending.set(session.key, { closeRequested: false }); try { - live.value.open(session, REELS_URL); + live.value.open(session, X_HOME_URL); } catch (error: unknown) { pending.delete(session.key); @@ -261,7 +261,7 @@ export default Plugin.define({ } }; - const closeReels = ( + const closeX = ( session: NonNullable> ): void => { const request = pending.get(session.key); @@ -271,26 +271,26 @@ export default Plugin.define({ request.closeRequested = true; } - const reelsTab = tabs.get(session.key); + const doomscrollTab = tabs.get(session.key); - if (reelsTab) { - void closeTab(session.key, reelsTab); + if (doomscrollTab) { + void closeTab(session.key, doomscrollTab); } }; ctx.add(Command, (): Command => ({ enabled: ctx.app.platform === "desktop" && !!ctx.sessions.current(), - id: "open-reels", - keywords: "instagram reels doomscroll break", + id: "open-x", + keywords: "x twitter doomscroll break", run: () => { const session = ctx.sessions.current(); if (session) { - openReels(session); + openX(session); } }, section: "session", - title: ctx.t(OPEN_REELS_KEY), + title: ctx.t(OPEN_X_KEY), })); ctx.add(SettingsPage, { @@ -321,7 +321,7 @@ export default Plugin.define({ queuedOpen.clear(); for (const session of sessions) { - openReels(session); + openX(session); } }, { @@ -353,7 +353,7 @@ export default Plugin.define({ const session = await findTopLevelSession(event.data.sessionID); if (session) { - openReels(session); + openX(session); } }) ), @@ -362,7 +362,7 @@ export default Plugin.define({ const session = await findTopLevelSession(event.data.sessionID); if (session) { - closeReels(session); + closeX(session); } }) ), From e654df1ffc1e4efaf18c0d97197e5f924aac5356 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:34:51 +1300 Subject: [PATCH 09/14] fix(doomscroll): satisfy consistent-return lint --- packages/opencode-doomscroll/src/gui.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode-doomscroll/src/gui.tsx b/packages/opencode-doomscroll/src/gui.tsx index 486a338..adf9f54 100644 --- a/packages/opencode-doomscroll/src/gui.tsx +++ b/packages/opencode-doomscroll/src/gui.tsx @@ -99,14 +99,14 @@ export default Plugin.define({ const session = findSession(sessionID); if (!session) { - return; + return null; } try { const info = await session.server.client.session.get({ sessionID }); if (info.parentID && !info.fork) { - return; + return null; } } catch { // The desktop's shell session reference is the fallback if the server From 9757ac6228876305576b4171b2acf4b6227f1ce1 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:35:12 +1300 Subject: [PATCH 10/14] fix(doomscroll): avoid ambiguous browser tab matches --- packages/opencode-doomscroll/src/gui.tsx | 30 +++++++----------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/packages/opencode-doomscroll/src/gui.tsx b/packages/opencode-doomscroll/src/gui.tsx index adf9f54..400ee1c 100644 --- a/packages/opencode-doomscroll/src/gui.tsx +++ b/packages/opencode-doomscroll/src/gui.tsx @@ -29,33 +29,19 @@ interface DoomscrollTab { } interface PendingOpen { - candidateTabID?: string; closeRequested: boolean; } const matchesTab = ( tab: BrowserTab, previous: ReadonlyMap | undefined, - request: PendingOpen, - focusedTabID: string | null -): boolean => { - if (request.candidateTabID === tab.id) { - return true; - } - - const isNewTab = previous !== undefined && !previous.has(tab.id); - // Browser.open selects the new tab. Match its ID even if the first state is - // blank or a redirect has already changed its URL. - const isNewFocusedTab = isNewTab && tab.id === focusedTabID; - - if (isNewFocusedTab) { - request.candidateTabID = tab.id; - - return true; - } - - return false; -}; + focusedTabID: string | null, + allowFocusFallback: boolean +): boolean => + allowFocusFallback && + previous !== undefined && + !previous.has(tab.id) && + tab.id === focusedTabID; export default Plugin.define({ i18n: { @@ -196,7 +182,7 @@ export default Plugin.define({ for (const [sessionKey, request] of pending) { const opened = browserTabs.find((tab) => - matchesTab(tab, previous, request, state.focusedTabID) + matchesTab(tab, previous, state.focusedTabID, pending.size === 1) ); if (opened) { From ac17991968b62fce17172d9daf8b45dd008f0b7c Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:35:30 +1300 Subject: [PATCH 11/14] fix(doomscroll): clear tabs on browser reset --- packages/opencode-doomscroll/src/gui.tsx | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/opencode-doomscroll/src/gui.tsx b/packages/opencode-doomscroll/src/gui.tsx index 400ee1c..2258909 100644 --- a/packages/opencode-doomscroll/src/gui.tsx +++ b/packages/opencode-doomscroll/src/gui.tsx @@ -161,15 +161,18 @@ export default Plugin.define({ } const { state } = event; + const browserTabs = state?.tabs ?? []; + const previous = states.get(binding); - if (!state) { - return; + if (state) { + states.set( + binding, + new Map(browserTabs.map((tab) => [tab.id, tab.url])) + ); + } else { + states.delete(binding); } - const { tabs: browserTabs } = state; - const previous = states.get(binding); - states.set(binding, new Map(browserTabs.map((tab) => [tab.id, tab.url]))); - for (const [sessionKey, doomscrollTab] of tabs) { if (doomscrollTab.binding !== binding) { continue; @@ -182,7 +185,12 @@ export default Plugin.define({ for (const [sessionKey, request] of pending) { const opened = browserTabs.find((tab) => - matchesTab(tab, previous, state.focusedTabID, pending.size === 1) + matchesTab( + tab, + previous, + state?.focusedTabID ?? null, + pending.size === 1 + ) ); if (opened) { From 6adf1c9587868055b9148b900b6e5d29c041115b Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:35:49 +1300 Subject: [PATCH 12/14] fix(doomscroll): expire stale tab opens --- packages/opencode-doomscroll/src/gui.tsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/opencode-doomscroll/src/gui.tsx b/packages/opencode-doomscroll/src/gui.tsx index 2258909..ebbbe3b 100644 --- a/packages/opencode-doomscroll/src/gui.tsx +++ b/packages/opencode-doomscroll/src/gui.tsx @@ -210,11 +210,15 @@ export default Plugin.define({ const openX = ( session: NonNullable> ): void => { - if ( - queuedOpen.has(session.key) || - pending.has(session.key) || - tabs.has(session.key) - ) { + const request = pending.get(session.key); + + if (request) { + request.closeRequested = false; + + return; + } + + if (queuedOpen.has(session.key) || tabs.has(session.key)) { return; } @@ -237,7 +241,13 @@ export default Plugin.define({ } queuedOpen.delete(session.key); - pending.set(session.key, { closeRequested: false }); + 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); From ccfbbbb633d991a7705338d51305338e17947d47 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:35:57 +1300 Subject: [PATCH 13/14] chore(knip): register doomscroll GUI entrypoint --- knip.json | 3 +++ 1 file changed, 3 insertions(+) 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"] } } } From 5088ed20128ce37376f849b1d815860661c88c84 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Wed, 28 Oct 2026 18:36:17 +1300 Subject: [PATCH 14/14] docs(doomscroll): align changeset with X timeline --- .changeset/40f76543.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/40f76543.md b/.changeset/40f76543.md index 1afd1ca..9f60beb 100644 --- a/.changeset/40f76543.md +++ b/.changeset/40f76543.md @@ -2,4 +2,4 @@ "@mynameistito/opencode-doomscroll": minor --- -Add an experimental GUI plugin that opens Instagram Reels while an OpenCode session runs and closes the tab when the run ends. +Add an experimental GUI plugin that opens X's home timeline while an OpenCode session runs and closes the tab when the run ends.