From 0ffc1471cc844166c8f3344f26d2841b6f03d9d1 Mon Sep 17 00:00:00 2001 From: Kyle Luo Date: Wed, 26 Aug 2026 14:37:51 +0000 Subject: [PATCH 1/4] A working devcontainer file running opencode on Node & Typescript template w/ bun and genHtml --- .devcontainer/devcontainer-lock.json | 9 +++++++ .devcontainer/devcontainer.json | 19 +++++++++++++++ bun.lock | 36 ---------------------------- packages/app/package.json | 1 + 4 files changed, 29 insertions(+), 36 deletions(-) create mode 100644 .devcontainer/devcontainer-lock.json create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 00000000..be560843 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,9 @@ +{ + "features": { + "ghcr.io/devcontainers-extra/features/bun:1": { + "version": "1.1.0", + "resolved": "ghcr.io/devcontainers-extra/features/bun@sha256:0624284ecaead9dd4c6654616a7f939cfa4ebcbc60593700a74e35b1767befa5", + "integrity": "sha256:0624284ecaead9dd4c6654616a7f939cfa4ebcbc60593700a74e35b1767befa5" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..560d09bd --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,19 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Node.js & TypeScript", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-node:5-24-trixie", + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers-extra/features/bun:1": {} + } + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "sudo apt-get update && sudo apt-get install -y lcov" + // Configure tool-specific properties. + // "customizations": {}, + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index 1e3a6425..47da2b1f 100644 --- a/bun.lock +++ b/bun.lock @@ -1442,8 +1442,6 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@bufbuild/protobuf": ["@bufbuild/protobuf@2.12.0", "", {}, "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA=="], "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.12.0", "", { "dependencies": { "@bufbuild/protobuf": "2.12.0", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-ORlDITp8AFUXzIhLRoMCG+ud+D3MPKWb5HQXBoskMMnjeyEjE1H1qLonVNPyOr8lkx3xSfYUo8a0dvOZJVAzow=="], @@ -3002,8 +3000,6 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.8", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.8", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.8", "vitest": "4.1.8" }, "optionalPeers": ["@vitest/browser"] }, "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@vitest/mocker": ["@vitest/mocker@4.1.7", "", { "dependencies": { "@vitest/spy": "4.1.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA=="], @@ -3124,8 +3120,6 @@ "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], - "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg=="], - "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], @@ -3610,8 +3604,6 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], "engine.io-client": ["engine.io-client@6.6.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.20.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg=="], @@ -4132,12 +4124,6 @@ "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], - "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], - - "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], - - "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], - "iterate-iterator": ["iterate-iterator@1.0.2", "", {}, "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw=="], "iterate-value": ["iterate-value@1.0.2", "", { "dependencies": { "es-get-iterator": "^1.0.2", "iterate-iterator": "^1.0.1" } }, "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ=="], @@ -4306,8 +4292,6 @@ "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], - "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], - "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="], "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], @@ -6122,10 +6106,6 @@ "@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], - "@standard-community/standard-json/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], - - "@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], - "@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -6154,10 +6134,6 @@ "@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], - "@vitest/coverage-v8/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], - - "@vitest/coverage-v8/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], - "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], @@ -6208,8 +6184,6 @@ "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - "ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], - "astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], "astro/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -6300,8 +6274,6 @@ "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], - "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "engine.io-client/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], "esast-util-from-js/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -6352,8 +6324,6 @@ "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], - "istanbul-reports/html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], - "js-beautify/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "js-beautify/nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="], @@ -6960,18 +6930,12 @@ "@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], - "@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@storybook/csf-plugin/unplugin/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "@storybook/csf-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - "@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="], - "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], diff --git a/packages/app/package.json b/packages/app/package.json index e2e97abe..28b63a67 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -23,6 +23,7 @@ "test:unit": "bun test --conditions=solid --only-failures --preload ./happydom.ts ./src", "test:browser": "bun test --conditions=browser --preload ./happydom.ts ./test-browser", "test:unit:watch": "bun test --conditions=solid --watch --preload ./happydom.ts ./src", + "coverage:html": "genhtml coverage/lcov.info -o coverage-html", "test:e2e": "playwright test", "test:e2e:local": "playwright test", "test:e2e:ui": "playwright test --ui", From bf9586a33c5ed4be3c74ed32abefcb6961dcf043 Mon Sep 17 00:00:00 2001 From: Kyle Luo Date: Sun, 6 Sep 2026 16:06:24 +0000 Subject: [PATCH 2/4] Function-level Qlty complexity warnings resolved--split extractPromptFromParts() into two main helpers --- packages/app/src/utils/prompt.ts | 279 ++++++++++++++++++------------- 1 file changed, 164 insertions(+), 115 deletions(-) diff --git a/packages/app/src/utils/prompt.ts b/packages/app/src/utils/prompt.ts index 67d32086..71d174aa 100644 --- a/packages/app/src/utils/prompt.ts +++ b/packages/app/src/utils/prompt.ts @@ -2,6 +2,8 @@ import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@o import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt" import { createLegacyBlobReference } from "@/utils/draft-store" +// Intermediate representation for inline file and agent references while the +// original prompt text is reconstructed. type Inline = | { type: "file" @@ -50,155 +52,202 @@ function textPartValue(parts: Part[]) { }, undefined) } -/** - * Extract prompt content from message parts for restoring into the prompt input. - * This is used by undo to restore the original user prompt. - */ -export function extractPromptFromParts(parts: Part[], opts?: { directory?: string; attachmentName?: string }): Prompt { - const textPart = textPartValue(parts) - const text = textPart?.text ?? "" - const directory = opts?.directory - const attachmentName = opts?.attachmentName ?? "attachment" +const toRelative = (path: string, directory: string | undefined) => { + if (!directory) return path - const toRelative = (path: string) => { - if (!directory) return path + const prefix = directory.endsWith("/") ? directory : directory + "/" + if (path.startsWith(prefix)) return path.slice(prefix.length) - const prefix = directory.endsWith("/") ? directory : directory + "/" - if (path.startsWith(prefix)) return path.slice(prefix.length) + if (path.startsWith(directory)) { + const next = path.slice(directory.length) + if (next.startsWith("/")) return next.slice(1) + return next + } - if (path.startsWith(directory)) { - const next = path.slice(directory.length) - if (next.startsWith("/")) return next.slice(1) - return next - } + return path +} - return path - } +// For use in reconstructPrompt(). Returns inline file and agent references +// separately from image attachments, which are appended after reconstruction. +function collectInlineParts(parts: Part[], + options: { + directory?: string + attachmentName: string },) + { const inline: Inline[] = [] const images: ImageAttachmentPart[] = [] for (const part of parts) { - if (part.type === "file") { - const filePart = part as FilePart - const sourceText = filePart.source?.text - if (sourceText) { - const value = sourceText.value - const start = sourceText.start - const end = sourceText.end - let path = value - if (value.startsWith("@")) path = value.slice(1) - if (!value.startsWith("@") && filePart.source && "path" in filePart.source) { - path = filePart.source.path - } - inline.push({ - type: "file", - start, - end, - value, - path: toRelative(path), - selection: selectionFromFileUrl(filePart.url), - }) - continue - } - - if (filePart.url.startsWith("data:")) { - images.push({ - type: "image", - id: filePart.id, - filename: filePart.filename ?? attachmentName, - mime: filePart.mime, - blob: createLegacyBlobReference(filePart.url), - }) - } - } - - if (part.type === "agent") { - const agentPart = part as MessageAgentPart - const source = agentPart.source - if (!source) continue - inline.push({ - type: "agent", - start: source.start, - end: source.end, - value: source.value, - name: agentPart.name, - }) - } + if (part.type === "file") { + const collected = collectFilePart(part as FilePart, options) + if (collected?.inline) inline.push(collected.inline) + if (collected?.image) images.push(collected.image) + continue } + if (part.type === "agent") { + const agent = collectAgentPart(part as MessageAgentPart) + if (agent) inline.push(agent) + } + // Sort original inline stuff (files and agents) + +} inline.sort((a, b) => { if (a.start !== b.start) return a.start - b.start return a.end - b.end }) - const result: Prompt = [] - let position = 0 - let cursor = 0 + return { inline, images } +} - const pushText = (content: string) => { - if (!content) return - result.push({ - type: "text", - content, - start: position, - end: position + content.length, - }) - position += content.length +// Converts a file part into either an inline reference or an image attachment. +function collectFilePart(filePart: FilePart, options: { directory?: string; attachmentName: string }) { + const sourceText = filePart.source?.text + if (sourceText) { + const value = sourceText.value + let path = value + if (value.startsWith("@")) path = value.slice(1) + if (!value.startsWith("@") && filePart.source && "path" in filePart.source) { + path = filePart.source.path + } + return { + inline: { + type: "file" as const, + start: sourceText.start, + end: sourceText.end, + value, + path: toRelative(path, options.directory), + selection: selectionFromFileUrl(filePart.url), + }, + } } - const pushFile = (item: Extract) => { - const content = item.value - const attachment: FileAttachmentPart = { - type: "file", - path: item.path, - content, - start: position, - end: position + content.length, - selection: item.selection, - } - result.push(attachment) - position += content.length + if (!filePart.url.startsWith("data:")) return undefined + return { + image: { + type: "image" as const, + id: filePart.id, + filename: filePart.filename ?? options.attachmentName, + mime: filePart.mime, + blob: createLegacyBlobReference(filePart.url), + }, + } +} + +// Converts an agent part into an inline reference when it has source text. +function collectAgentPart(agentPart: MessageAgentPart): Extract | undefined { + const source = agentPart.source + if (!source) return undefined + return { + type: "agent", + start: source.start, + end: source.end, + value: source.value, + name: agentPart.name, } +} - const pushAgent = (item: Extract) => { - const content = item.value - const mention: AgentPart = { - type: "agent", - name: item.name, - content, - start: position, - end: position + content.length, - } - result.push(mention) - position += content.length +// Finds an inline reference in the text, falling back to a search if its +// original offsets no longer match. +function findInlineMatch(text: string, item: Inline, cursor: number) { + if (item.start < 0 || item.end < item.start || !item.value) return undefined + const mismatch = item.end > text.length || item.start < cursor || text.slice(item.start, item.end) !== item.value + const start = mismatch ? text.indexOf(item.value, cursor) : item.start + if (start === -1) return undefined + return { start, end: mismatch ? start + item.value.length : item.end } +} + +// Rebuilds the prompt by interleaving ordinary text with inline file and agent parts. +function reconstructPrompt(text: string, inline: Inline[]):Prompt { + let result: Prompt = [] + let position = 0 + let cursor = 0 + +// Helper to push prompt-class readable JSON onto the result +const pushText = (content: string) => { + if (!content) return + result.push({ + type: "text", + content, + start: position, + end: position + content.length, + }) + position += content.length +} +// Helper to push prompt-class readable JSON onto the result +const pushFile = (item: Extract) => { + const content = item.value + const attachment: FileAttachmentPart = { + type: "file", + path: item.path, + content, + start: position, + end: position + content.length, + selection: item.selection, } + result.push(attachment) + position += content.length +} +// Helper to push prompt-class readable JSON onto the result +const pushAgent = (item: Extract) => { + const content = item.value + const mention: AgentPart = { + type: "agent", + name: item.name, + content, + start: position, + end: position + content.length, + } + result.push(mention) + position += content.length +} - for (const item of inline) { - if (item.start < 0 || item.end < item.start) continue +for (const item of inline) { + // Find an Inline item + const match = findInlineMatch(text, item, cursor) + if (!match) continue - const expected = item.value - if (!expected) continue + // Push all ordinary text before the item + pushText(text.slice(cursor, match.start)) - const mismatch = item.end > text.length || item.start < cursor || text.slice(item.start, item.end) !== expected - const start = mismatch ? text.indexOf(expected, cursor) : item.start - if (start === -1) continue - const end = mismatch ? start + expected.length : item.end + if (item.type === "file") pushFile(item) + if (item.type === "agent") pushAgent(item) - pushText(text.slice(cursor, start)) + cursor = match.end +} - if (item.type === "file") pushFile(item) - if (item.type === "agent") pushAgent(item) +// After all the inline objects are pushed, push the remaining text. +pushText(text.slice(cursor)) +return result +} - cursor = end - } +/** + * Extract prompt content from message parts for restoring into the prompt input. + * This is used by undo to restore the original user prompt. + */ +export function extractPromptFromParts(parts: Part[], opts?: { directory?: string; attachmentName?: string }): Prompt { + + const textPart = textPartValue(parts) + const text = textPart?.text ?? "" + const directory = opts?.directory + const attachmentName = opts?.attachmentName ?? "attachment" + + const { inline, images } = collectInlineParts(parts, { + directory: opts?.directory, + attachmentName, + }) - pushText(text.slice(cursor)) + // Construct the result prompt + let result: Prompt = [] + result = reconstructPrompt(text, inline) + // If the result has nothing, say empty text is the default return. if (result.length === 0) { result.push({ type: "text", content: "", start: 0, end: 0 }) } + // If the result has nothing, append images if (images.length === 0) return result return [...result, ...images] } From c532a0c3025ca76ceb0e656a5a46f7a38a2bd85d Mon Sep 17 00:00:00 2001 From: Kyle Luo Date: Sun, 6 Sep 2026 16:44:19 +0000 Subject: [PATCH 3/4] Confirm test coverage for extractPromptFromParts refactor --- packages/app/src/utils/prompt.test.ts | 131 ++++++++++++++++++++++++++ packages/app/src/utils/prompt.ts | 1 - 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/packages/app/src/utils/prompt.test.ts b/packages/app/src/utils/prompt.test.ts index 8b86d43b..30e6b6f0 100644 --- a/packages/app/src/utils/prompt.test.ts +++ b/packages/app/src/utils/prompt.test.ts @@ -3,6 +3,8 @@ import type { Part } from "@opencode-ai/sdk/v2" import { extractPromptFromParts } from "./prompt" describe("extractPromptFromParts", () => { + const message = { sessionID: "ses_1", messageID: "msg_1" } + test("restores multiple uploaded attachments", () => { const parts = [ { @@ -51,4 +53,133 @@ describe("extractPromptFromParts", () => { }, ]) }) + + test("restores files without text", () => { + const parts = [ + { + ...message, + id: "file_1", + type: "file", + mime: "image/png", + url: "data:image/png;base64,AAA", + filename: "image.png", + }, + { + ...message, + id: "file_2", + type: "file", + mime: "application/pdf", + url: "data:application/pdf;base64,BBB", + filename: "document.pdf", + }, + ] satisfies Part[] + + const result = extractPromptFromParts(parts) + + expect(result).toHaveLength(3) + expect(result[0]).toMatchObject({ type: "text", content: "" }) + expect(result.slice(1)).toMatchObject([ + { type: "image", filename: "image.png", mime: "image/png" }, + { type: "image", filename: "document.pdf", mime: "application/pdf" }, + ]) + }) + + test("interleaves inline files and text", () => { + const text = "read @src/app.ts then check @src/config.ts" + const first = "@src/app.ts" + const second = "@src/config.ts" + const parts = [ + { ...message, id: "text_1", type: "text", text }, + { + ...message, + id: "file_1", + type: "file", + mime: "text/plain", + url: "file:///workspace/src/app.ts", + source: { type: "file", path: "src/app.ts", text: { value: first, start: 5, end: 16 } }, + }, + { + ...message, + id: "file_2", + type: "file", + mime: "text/plain", + url: "file:///workspace/src/config.ts", + source: { type: "file", path: "src/config.ts", text: { value: second, start: 28, end: 43 } }, + }, + ] satisfies Part[] + + const result = extractPromptFromParts(parts) + + expect(result).toMatchObject([ + { type: "text", content: "read " }, + { type: "file", path: "src/app.ts", content: first }, + { type: "text", content: " then check " }, + { type: "file", path: "src/config.ts", content: second }, + ]) + }) + + test("interleaves agent mentions and text", () => { + const text = "ask @planner to review" + const parts = [ + { ...message, id: "text_1", type: "text", text }, + { + ...message, + id: "agent_1", + type: "agent", + name: "planner", + source: { value: "@planner", start: 4, end: 12 }, + }, + ] satisfies Part[] + + expect(extractPromptFromParts(parts)).toMatchObject([ + { type: "text", content: "ask " }, + { type: "agent", name: "planner", content: "@planner" }, + { type: "text", content: " to review" }, + ]) + }) + + test("restores a prompt with only images", () => { + const parts = [ + { + ...message, + id: "file_1", + type: "file", + mime: "image/png", + url: "data:image/png;base64,AAA", + }, + ] satisfies Part[] + + const result = extractPromptFromParts(parts) + + expect(result).toHaveLength(2) + expect(result[0]).toMatchObject({ type: "text", content: "" }) + expect(result[1]).toMatchObject({ type: "image", mime: "image/png" }) + }) + + test("restores text when there are no inline parts", () => { + const parts = [{ ...message, id: "text_1", type: "text", text: "just text" }] satisfies Part[] + + expect(extractPromptFromParts(parts)).toMatchObject([{ type: "text", content: "just text" }]) + }) + + // Stale offsets occur when metadata was recorded for an earlier version of + // the prompt. The value is still present, so reconstruction searches for it. + test("recovers inline files with stale offsets", () => { + const parts = [ + { ...message, id: "text_1", type: "text", text: "use @src/app.ts" }, + { + ...message, + id: "file_1", + type: "file", + mime: "text/plain", + url: "file:///workspace/src/app.ts", + source: { type: "file", path: "src/app.ts", text: { value: "@src/app.ts", start: 0, end: 11 } }, + }, + ] satisfies Part[] + + expect(extractPromptFromParts(parts)).toMatchObject([ + { type: "text", content: "use " }, + { type: "file", path: "src/app.ts", content: "@src/app.ts" }, + ]) + }) }) diff --git a/packages/app/src/utils/prompt.ts b/packages/app/src/utils/prompt.ts index 71d174aa..9e9ee8dd 100644 --- a/packages/app/src/utils/prompt.ts +++ b/packages/app/src/utils/prompt.ts @@ -230,7 +230,6 @@ export function extractPromptFromParts(parts: Part[], opts?: { directory?: strin const textPart = textPartValue(parts) const text = textPart?.text ?? "" - const directory = opts?.directory const attachmentName = opts?.attachmentName ?? "attachment" const { inline, images } = collectInlineParts(parts, { From 92ef8a54def15101cca811366fd584feb8d0260b Mon Sep 17 00:00:00 2001 From: Kyle Luo Date: Sun, 6 Sep 2026 18:15:00 +0000 Subject: [PATCH 4/4] Fixed linter issue with unused variable --- packages/app/src/utils/prompt.ts | 71 ++++++++++++++++---------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/packages/app/src/utils/prompt.ts b/packages/app/src/utils/prompt.ts index 9e9ee8dd..efb86770 100644 --- a/packages/app/src/utils/prompt.ts +++ b/packages/app/src/utils/prompt.ts @@ -26,6 +26,35 @@ type Inline = name: string } +/** +* Extract prompt content from message parts for restoring into the prompt input. +* This is used by undo to restore the original user prompt. +*/ +export function extractPromptFromParts(parts: Part[], opts?: { directory?: string; attachmentName?: string }): Prompt { + + const textPart = textPartValue(parts) + const text = textPart?.text ?? "" + const attachmentName = opts?.attachmentName ?? "attachment" + + const { inline, images } = collectInlineParts(parts, { + directory: opts?.directory, + attachmentName, + }) + + // Construct the result prompt + let result: Prompt = [] + result = reconstructPrompt(text, inline) + + // If the result has nothing, say empty text is the default return. + if (result.length === 0) { + result.push({ type: "text", content: "", start: 0, end: 0 }) + } + + // If the result has nothing, append images + if (images.length === 0) return result + return [...result, ...images] +} + function selectionFromFileUrl(url: string): Extract["selection"] { const queryIndex = url.indexOf("?") if (queryIndex === -1) return undefined @@ -148,8 +177,7 @@ function collectAgentPart(agentPart: MessageAgentPart): Extract text.length || item.start < cursor || text.slice(item.start, item.end) !== item.value @@ -164,7 +192,7 @@ function reconstructPrompt(text: string, inline: Inline[]):Prompt { let position = 0 let cursor = 0 -// Helper to push prompt-class readable JSON onto the result + const pushText = (content: string) => { if (!content) return result.push({ @@ -175,7 +203,7 @@ const pushText = (content: string) => { }) position += content.length } -// Helper to push prompt-class readable JSON onto the result + const pushFile = (item: Extract) => { const content = item.value const attachment: FileAttachmentPart = { @@ -189,7 +217,7 @@ const pushFile = (item: Extract) => { result.push(attachment) position += content.length } -// Helper to push prompt-class readable JSON onto the result + const pushAgent = (item: Extract) => { const content = item.value const mention: AgentPart = { @@ -204,11 +232,11 @@ const pushAgent = (item: Extract) => { } for (const item of inline) { - // Find an Inline item + const match = findInlineMatch(text, item, cursor) if (!match) continue - // Push all ordinary text before the item + pushText(text.slice(cursor, match.start)) if (item.type === "file") pushFile(item) @@ -217,36 +245,9 @@ for (const item of inline) { cursor = match.end } -// After all the inline objects are pushed, push the remaining text. + pushText(text.slice(cursor)) return result } -/** - * Extract prompt content from message parts for restoring into the prompt input. - * This is used by undo to restore the original user prompt. - */ -export function extractPromptFromParts(parts: Part[], opts?: { directory?: string; attachmentName?: string }): Prompt { - - const textPart = textPartValue(parts) - const text = textPart?.text ?? "" - const attachmentName = opts?.attachmentName ?? "attachment" - - const { inline, images } = collectInlineParts(parts, { - directory: opts?.directory, - attachmentName, - }) - - // Construct the result prompt - let result: Prompt = [] - result = reconstructPrompt(text, inline) - - // If the result has nothing, say empty text is the default return. - if (result.length === 0) { - result.push({ type: "text", content: "", start: 0, end: 0 }) - } - // If the result has nothing, append images - if (images.length === 0) return result - return [...result, ...images] -}