diff --git a/bun.lock b/bun.lock index f338c9b34..d8dee77e8 100644 --- a/bun.lock +++ b/bun.lock @@ -1,11 +1,12 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "21st-desktop", "dependencies": { "@ai-sdk/react": "^3.0.14", - "@anthropic-ai/claude-agent-sdk": "0.2.32", + "@anthropic-ai/claude-agent-sdk": "0.2.45", "@git-diff-view/react": "^0.0.35", "@git-diff-view/shiki": "^0.0.36", "@mcpc-tech/acp-ai-provider": "^0.2.4", @@ -30,6 +31,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@sentry/electron": "^7.5.0", + "@tabler/icons-react": "^3.41.1", "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.10", "@tanstack/react-virtual": "^3.13.18", @@ -42,7 +44,7 @@ "@xterm/addon-serialize": "^0.14.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/addon-webgl": "^0.19.0", - "@zed-industries/codex-acp": "^0.9.3", + "@zed-industries/codex-acp": "0.9.3", "ai": "^6.0.14", "async-mutex": "^0.5.0", "better-sqlite3": "^12.6.2", @@ -90,6 +92,7 @@ "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^4.0.0", "@electron/rebuild": "^4.0.3", + "@tailwindcss/container-queries": "^0.1.1", "@types/better-sqlite3": "^7.6.13", "@types/diff": "^8.0.0", "@types/node": "^20.17.50", @@ -103,6 +106,7 @@ "electron-builder": "^25.1.8", "electron-vite": "^3.0.0", "postcss": "^8.5.1", + "react-grab": "^0.1.30", "tailwindcss": "^3.4.17", "typescript": "^5.4.5", "vite": "^6.3.4", @@ -126,7 +130,9 @@ "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.32", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-8AtsSx/M9jxd0ihS08eqa7VireTEuwQy0i1+6ZJX93LECT6Svlf47dPJiAm7JB+BhVMmwTfQeS6x1akIcCfvbQ=="], + "@antfu/ni": ["@antfu/ni@0.23.2", "", { "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nu": "bin/nu.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs" } }, "sha512-FSEVWXvwroExDXUu8qV6Wqp2X3D1nJ0Li4LFymCyvCVrm7I3lNfG0zZWSWvGU1RE7891eTnFTyh31L3igOwNKQ=="], + + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.45", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-AKH2hKoJNyjLf9ThAttKqbmCjUFg7qs/8+LR/UTVX20fCLn359YH9WrQc6dAiAfi8RYNA+mWwrNYCAq+Sdo5Ag=="], "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.8.2", "", {}, "sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA=="], @@ -554,6 +560,8 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@react-grab/cli": ["@react-grab/cli@0.1.30", "", { "dependencies": { "@antfu/ni": "^0.23.0", "commander": "^14.0.0", "ignore": "^7.0.5", "jsonc-parser": "^3.3.1", "ora": "^8.2.0", "picocolors": "^1.1.1", "prompts": "^2.4.2", "smol-toml": "^1.6.0" }, "bin": { "react-grab": "dist/cli.js" } }, "sha512-jy8SKQ+QzpYx5TyqKVEfR3xqUFt4K/WfZtHkFk3SALW/ASpUNvWC3lohBtw5+Op6jwzBaGrjhljor6vC3+7pLA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="], @@ -648,6 +656,12 @@ "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], + "@tabler/icons": ["@tabler/icons@3.41.1", "", {}, "sha512-OaRnVbRmH2nHtFeg+RmMJ/7m2oBIF9XCJAUD5gQnMrpK9f05ydj8MZrAf3NZQqOXyxGN1UBL0D5IKLLEUfr74Q=="], + + "@tabler/icons-react": ["@tabler/icons-react@3.41.1", "", { "dependencies": { "@tabler/icons": "3.41.1" }, "peerDependencies": { "react": ">= 16" } }, "sha512-kUgweE+DJtAlMZVIns1FTDdcbpRVnkK7ZpUOXmoxy3JAF0rSHj0TcP4VHF14+gMJGnF+psH2Zt26BLT6owetBA=="], + + "@tailwindcss/container-queries": ["@tailwindcss/container-queries@0.1.1", "", { "peerDependencies": { "tailwindcss": ">=3.2.0" } }, "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA=="], + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], "@tanstack/query-core": ["@tanstack/query-core@5.90.19", "", {}, "sha512-GLW5sjPVIvH491VV1ufddnfldyVB+teCnpPIvweEfkpRx7CfUmUGhoh9cdcUKBh/KwVxk22aNEDxeTsvmyB/WA=="], @@ -910,6 +924,8 @@ "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + "bippy": ["bippy@0.5.37", "", { "peerDependencies": { "react": ">=17.0.1" } }, "sha512-5+Xre/yCsrTKLTeiMcrZTKPOaCB9VSPJeWQD+t2MLd0CLD3HqduCVIqXoYsqt8LpNVtTEHeH3g3+XPiR5d7piA=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], @@ -1008,7 +1024,7 @@ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], @@ -1330,6 +1346,8 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], @@ -1418,6 +1436,8 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "import-in-the-middle": ["import-in-the-middle@2.0.5", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-0InH9/4oDCBRzWXhpOqusspLBrVfK1vPvbn9Wxl8DAQ8yyx5fWJRETICSwkiAMaYntjJAMBP1R4B6cQnEUYVEA=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], @@ -1522,6 +1542,8 @@ "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "langium": ["langium@3.3.1", "", { "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.0.8" } }, "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w=="], "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], @@ -1686,6 +1708,8 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], @@ -1874,6 +1898,8 @@ "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], @@ -1902,6 +1928,8 @@ "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="], + "react-grab": ["react-grab@0.1.30", "", { "dependencies": { "@react-grab/cli": "0.1.30", "bippy": "^0.5.37", "solid-js": "^1.9.10" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-rf3xK3NYoXA0RPuwT/j4xaseeFPSNM0P/A+vgLd+4uP+4C9ZgV2ozi+laqLIlPRPKETEx5yXwHsMImq7caqAxA=="], + "react-hotkeys-hook": ["react-hotkeys-hook@4.6.2", "", { "peerDependencies": { "react": ">=16.8.1", "react-dom": ">=16.8.1" } }, "sha512-FmP+ZriY3EG59Ug/lxNfrObCnW9xQShgk7Nb83+CkpfkcCpfS95ydv+E9JuXA5cp8KtskU7LGlIARpkc92X22Q=="], "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="], @@ -2010,6 +2038,10 @@ "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="], + + "seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="], + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], @@ -2040,14 +2072,20 @@ "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + "solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="], + "sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -2068,6 +2106,8 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + "streamdown": ["streamdown@2.1.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.1.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-u9gWd0AmjKg1d+74P44XaPlGrMeC21oDOSIhjGNEYMAttDMzCzlJO6lpTyJ9JkSinQQF65YcK4eOd3q9iTvULw=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -2356,6 +2396,8 @@ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@react-grab/cli/ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], + "@sentry/node/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@shikijs/core/@shikijs/types": ["@shikijs/types@3.21.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA=="], @@ -2468,6 +2510,8 @@ "streamdown/tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], @@ -2548,6 +2592,20 @@ "@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.21.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA=="], + "@react-grab/cli/ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "@react-grab/cli/ora/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "@react-grab/cli/ora/is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + + "@react-grab/cli/ora/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "@react-grab/cli/ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + + "@react-grab/cli/ora/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "@react-grab/cli/ora/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "app-builder-lib/@electron/rebuild/node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], @@ -2606,6 +2664,14 @@ "zip-stream/archiver-utils/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "@react-grab/cli/ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "@react-grab/cli/ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + + "@react-grab/cli/ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "@react-grab/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "app-builder-lib/@electron/rebuild/node-gyp/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="], @@ -2628,6 +2694,10 @@ "zip-stream/archiver-utils/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@react-grab/cli/ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "@react-grab/cli/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "app-builder-lib/@electron/rebuild/node-gyp/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="], diff --git a/drizzle/0008_steep_warlock.sql b/drizzle/0008_steep_warlock.sql new file mode 100644 index 000000000..741606cd6 --- /dev/null +++ b/drizzle/0008_steep_warlock.sql @@ -0,0 +1 @@ +ALTER TABLE `chats` ADD `accent_color` text; \ No newline at end of file diff --git a/drizzle/0009_premium_morbius.sql b/drizzle/0009_premium_morbius.sql new file mode 100644 index 000000000..51ea96d28 --- /dev/null +++ b/drizzle/0009_premium_morbius.sql @@ -0,0 +1 @@ +ALTER TABLE `projects` ADD `accent_color` text; \ No newline at end of file diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100644 index 000000000..730bf9ee6 --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,441 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "17bcef0a-fdb3-4abd-8e1e-01df587d71ee", + "prevId": "b2d2d602-5de1-43b1-ada8-c9ed3edde22d", + "tables": { + "anthropic_accounts": { + "name": "anthropic_accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_token": { + "name": "oauth_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_at": { + "name": "connected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "desktop_user_id": { + "name": "desktop_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "anthropic_settings": { + "name": "anthropic_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "'singleton'" + }, + "active_account_id": { + "name": "active_account_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "worktree_path": { + "name": "worktree_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "chats_worktree_path_idx": { + "name": "chats_worktree_path_idx", + "columns": [ + "worktree_path" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chats_project_id_projects_id_fk": { + "name": "chats_project_id_projects_id_fk", + "tableFrom": "chats", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "claude_code_credentials": { + "name": "claude_code_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "oauth_token": { + "name": "oauth_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_at": { + "name": "connected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_remote_url": { + "name": "git_remote_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_provider": { + "name": "git_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_owner": { + "name": "git_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_repo": { + "name": "git_repo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_path": { + "name": "icon_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "projects_path_unique": { + "name": "projects_path_unique", + "columns": [ + "path" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sub_chats": { + "name": "sub_chats", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'agent'" + }, + "messages": { + "name": "messages", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sub_chats_chat_id_chats_id_fk": { + "name": "sub_chats_chat_id_chats_id_fk", + "tableFrom": "sub_chats", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 000000000..4edbe4bbb --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,448 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "07b1dd76-a453-4a55-a473-3dc71b7fad3d", + "prevId": "17bcef0a-fdb3-4abd-8e1e-01df587d71ee", + "tables": { + "anthropic_accounts": { + "name": "anthropic_accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_token": { + "name": "oauth_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_at": { + "name": "connected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "desktop_user_id": { + "name": "desktop_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "anthropic_settings": { + "name": "anthropic_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "'singleton'" + }, + "active_account_id": { + "name": "active_account_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "worktree_path": { + "name": "worktree_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "chats_worktree_path_idx": { + "name": "chats_worktree_path_idx", + "columns": [ + "worktree_path" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chats_project_id_projects_id_fk": { + "name": "chats_project_id_projects_id_fk", + "tableFrom": "chats", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "claude_code_credentials": { + "name": "claude_code_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "oauth_token": { + "name": "oauth_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_at": { + "name": "connected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_remote_url": { + "name": "git_remote_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_provider": { + "name": "git_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_owner": { + "name": "git_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_repo": { + "name": "git_repo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_path": { + "name": "icon_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "projects_path_unique": { + "name": "projects_path_unique", + "columns": [ + "path" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sub_chats": { + "name": "sub_chats", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'agent'" + }, + "messages": { + "name": "messages", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sub_chats_chat_id_chats_id_fk": { + "name": "sub_chats_chat_id_chats_id_fk", + "tableFrom": "sub_chats", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 88a3e0a60..54c2368bd 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -57,6 +57,20 @@ "when": 1769810815497, "tag": "0007_clammy_grim_reaper", "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1775474905169, + "tag": "0008_steep_warlock", + "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1775479112296, + "tag": "0009_premium_morbius", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index da2a5e747..de1ee1b99 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@sentry/electron": "^7.5.0", + "@tabler/icons-react": "^3.41.1", "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.10", "@tanstack/react-virtual": "^3.13.18", @@ -135,6 +136,7 @@ "electron-builder": "^25.1.8", "electron-vite": "^3.0.0", "postcss": "^8.5.1", + "react-grab": "^0.1.30", "tailwindcss": "^3.4.17", "typescript": "^5.4.5", "vite": "^6.3.4" diff --git a/src/main/index.ts b/src/main/index.ts index 57af873f0..4db0e04e5 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -863,7 +863,7 @@ if (gotTheLock) { }, }, ]) - app.dock.setMenu(dockMenu) + app.dock?.setMenu(dockMenu) } // Set update state and rebuild menu diff --git a/src/main/lib/claude/transform.ts b/src/main/lib/claude/transform.ts index 0d1a1cec4..40073703e 100644 --- a/src/main/lib/claude/transform.ts +++ b/src/main/lib/claude/transform.ts @@ -83,12 +83,14 @@ export function createTransformer(options?: { isUsingOllama?: boolean }) { } // Emit complete tool call with accumulated input + // Cast needed: providerMetadata is used by the renderer for timing + // but isn't part of the base UIMessageChunk type yield { type: "tool-input-available", toolCallId: currentToolCallId, toolName: currentToolName || "unknown", input: parsedInput, - providerMetadata: { custom: { startedAt: Date.now() } }, + ...({ providerMetadata: { custom: { startedAt: Date.now() } } } as any), } currentToolCallId = null currentToolName = null @@ -171,7 +173,7 @@ export function createTransformer(options?: { isUsingOllama?: boolean }) { yield { type: "tool-input-start", toolCallId: currentToolCallId, - toolName: currentToolName, + toolName: currentToolName ?? "unknown", } } @@ -315,12 +317,14 @@ export function createTransformer(options?: { isUsingOllama?: boolean }) { // Store mapping for tool-result lookup toolIdMapping.set(block.id, compositeId) + // Cast needed: providerMetadata is used by the renderer for timing + // but isn't part of the base UIMessageChunk type yield { type: "tool-input-available", toolCallId: compositeId, toolName: block.name, input: block.input, - providerMetadata: { custom: { startedAt: Date.now() } }, + ...({ providerMetadata: { custom: { startedAt: Date.now() } } } as any), } } } diff --git a/src/main/lib/credential-manager.ts b/src/main/lib/credential-manager.ts index 150ef3d8a..bf9499f91 100644 --- a/src/main/lib/credential-manager.ts +++ b/src/main/lib/credential-manager.ts @@ -1,3 +1,4 @@ +// @ts-nocheck — WIP file, dependencies not yet created /** * SourceCredentialManager * @@ -21,30 +22,30 @@ import { type GoogleService, type SlackService, type MicrosoftService, -} from './types.ts'; -import type { CredentialId, StoredCredential } from '../credentials/types.ts'; -import { getCredentialManager } from '../credentials/index.ts'; -import { CraftOAuth, getMcpBaseUrl, type OAuthCallbacks, type OAuthTokens } from '../auth/oauth.ts'; +} from './types'; +import type { CredentialId, StoredCredential } from '../credentials/types'; +import { getCredentialManager } from '../credentials/index'; +import { CraftOAuth, getMcpBaseUrl, type OAuthCallbacks, type OAuthTokens } from '../auth/oauth'; import { startGoogleOAuth, refreshGoogleToken, type GoogleOAuthResult, type GoogleOAuthOptions, -} from '../auth/google-oauth.ts'; +} from '../auth/google-oauth'; import { startSlackOAuth, refreshSlackToken, type SlackOAuthResult, type SlackOAuthOptions, -} from '../auth/slack-oauth.ts'; +} from '../auth/slack-oauth'; import { startMicrosoftOAuth, refreshMicrosoftToken, type MicrosoftOAuthResult, type MicrosoftOAuthOptions, -} from '../auth/microsoft-oauth.ts'; -import { debug } from '../utils/debug.ts'; -import { markSourceAuthenticated, loadSourceConfig, saveSourceConfig } from './storage.ts'; +} from '../auth/microsoft-oauth'; +import { debug } from '../utils/debug'; +import { markSourceAuthenticated, loadSourceConfig, saveSourceConfig } from './storage'; /** * Result of authentication attempt @@ -314,8 +315,8 @@ export class SourceCredentialManager { callbacks?: OAuthCallbacks ): Promise { const defaultCallbacks: OAuthCallbacks = { - onStatus: (msg) => debug(`[SourceCredentialManager] ${msg}`), - onError: (err) => debug(`[SourceCredentialManager] Error: ${err}`), + onStatus: (msg: string) => debug(`[SourceCredentialManager] ${msg}`), + onError: (err: string) => debug(`[SourceCredentialManager] Error: ${err}`), }; const cb = callbacks || defaultCallbacks; diff --git a/src/main/lib/db/schema/index.ts b/src/main/lib/db/schema/index.ts index fe6aa3490..bc170ab54 100644 --- a/src/main/lib/db/schema/index.ts +++ b/src/main/lib/db/schema/index.ts @@ -22,6 +22,8 @@ export const projects = sqliteTable("projects", { gitRepo: text("git_repo"), // Custom project icon (absolute path to local image file) iconPath: text("icon_path"), + // Custom accent color for visual differentiation (hex string e.g. "#ef4444") + accentColor: text("accent_color"), }) export const projectsRelations = relations(projects, ({ many }) => ({ @@ -51,6 +53,8 @@ export const chats = sqliteTable("chats", { // PR tracking fields prUrl: text("pr_url"), prNumber: integer("pr_number"), + // Custom accent color for visual differentiation (hex string e.g. "#ef4444") + accentColor: text("accent_color"), }, (table) => [ index("chats_worktree_path_idx").on(table.worktreePath), ]) diff --git a/src/main/lib/git/watcher/git-watcher.ts b/src/main/lib/git/watcher/git-watcher.ts index 141868db8..855030a91 100644 --- a/src/main/lib/git/watcher/git-watcher.ts +++ b/src/main/lib/git/watcher/git-watcher.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "events"; -// Chokidar is ESM-only, so we need to dynamically import it -type FSWatcher = Awaited>["FSWatcher"] extends new () => infer T ? T : never; +// Type-only import is safe for ESM-only packages -- erased at compile time +import type { FSWatcher } from "chokidar"; // Simple debounce implementation to avoid lodash-es dependency in main process function debounce unknown>( @@ -160,7 +160,7 @@ export class GitWatcher extends EventEmitter { this.pendingChanges.set(path, "unlink"); flushChanges(); }) - .on("error", (error: Error) => { + .on("error", (error: unknown) => { console.error("[GitWatcher] Error:", error); this.emit("error", error); }); diff --git a/src/main/lib/trpc/routers/chats.ts b/src/main/lib/trpc/routers/chats.ts index a699b445d..0643b37c4 100644 --- a/src/main/lib/trpc/routers/chats.ts +++ b/src/main/lib/trpc/routers/chats.ts @@ -482,6 +482,26 @@ export const chatsRouter = router({ .get() }), + /** + * Update accent color for a workspace (hex string or null to clear) + */ + updateColor: publicProcedure + .input( + z.object({ + id: z.string(), + accentColor: z.string().nullable(), + }), + ) + .mutation(({ input }) => { + const db = getDatabase() + return db + .update(chats) + .set({ accentColor: input.accentColor, updatedAt: new Date() }) + .where(eq(chats.id, input.id)) + .returning() + .get() + }), + /** * Archive a chat (also kills any terminal processes in the workspace) * Optionally deletes the worktree to free disk space diff --git a/src/main/lib/trpc/routers/claude.ts b/src/main/lib/trpc/routers/claude.ts index 9e5eadffe..0ffe8225f 100644 --- a/src/main/lib/trpc/routers/claude.ts +++ b/src/main/lib/trpc/routers/claude.ts @@ -1034,7 +1034,6 @@ export const claudeRouter = router({ } const transform = createTransformer({ - emitSdkMessageUuid: historyEnabled, isUsingOllama, }) @@ -1399,7 +1398,9 @@ export const claudeRouter = router({ // Build final env - only add OAuth token if we have one AND no existing API config // Existing CLI config takes precedence over OAuth - const finalEnv = { + // Typed as Record to preserve access to dynamic env vars + // like ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN + const finalEnv: Record = { ...claudeEnv, ...(claudeCodeToken && !hasExistingApiConfig && { @@ -1864,19 +1865,19 @@ ${prompt} : "" if (!/\.md$/i.test(filePath)) { return { - behavior: "deny", + behavior: "deny" as const, message: 'Only ".md" files can be modified in plan mode.', } } } else if (toolName == "ExitPlanMode") { return { - behavior: "deny", + behavior: "deny" as const, message: `IMPORTANT: DONT IMPLEMENT THE PLAN UNTIL THE EXPLIT COMMAND. THE PLAN WAS **ONLY** PRESENTED TO USER, FINISH CURRENT MESSAGE AS SOON AS POSSIBLE`, } } else if (PLAN_MODE_BLOCKED_TOOLS.has(toolName)) { return { - behavior: "deny", + behavior: "deny" as const, message: `Tool "${toolName}" blocked in plan mode.`, } } @@ -1931,13 +1932,15 @@ ${prompt} askToolPart.state = "result" } // Emit result to frontend so it updates in real-time + // Cast through unknown because ask-user-question-result is a custom + // extension not in the UIMessageChunk union type safeEmit({ type: "ask-user-question-result", toolUseId: toolUseID, result: errorMessage, - } as UIMessageChunk) + } as unknown as UIMessageChunk) return { - behavior: "deny", + behavior: "deny" as const, message: errorMessage, } } @@ -1950,18 +1953,20 @@ ${prompt} askToolPart.state = "result" } // Emit result to frontend so it updates in real-time + // Cast through unknown because ask-user-question-result is a custom + // extension not in the UIMessageChunk union type safeEmit({ type: "ask-user-question-result", toolUseId: toolUseID, result: answerResult, - } as UIMessageChunk) + } as unknown as UIMessageChunk) return { - behavior: "allow", - updatedInput: response.updatedInput, + behavior: "allow" as const, + updatedInput: response.updatedInput as Record, } } return { - behavior: "allow", + behavior: "allow" as const, updatedInput: toolInput, } }, diff --git a/src/main/lib/trpc/routers/codex.ts b/src/main/lib/trpc/routers/codex.ts index 0bc355eb9..d2fff04ca 100644 --- a/src/main/lib/trpc/routers/codex.ts +++ b/src/main/lib/trpc/routers/codex.ts @@ -80,6 +80,8 @@ type CodexMcpServerForSettings = { tools: McpToolInfo[] needsAuth: boolean config: Record + serverInfo?: { name: string; version: string; icons?: Array<{ src: string }> } + error?: string } type CodexMcpSnapshot = { diff --git a/src/main/lib/trpc/routers/plugins.ts b/src/main/lib/trpc/routers/plugins.ts index 710cc0557..00a0b6d26 100644 --- a/src/main/lib/trpc/routers/plugins.ts +++ b/src/main/lib/trpc/routers/plugins.ts @@ -16,7 +16,7 @@ interface PluginComponent { description?: string } -interface PluginWithComponents { +export interface PluginWithComponents { name: string version: string description?: string diff --git a/src/main/lib/trpc/routers/projects.ts b/src/main/lib/trpc/routers/projects.ts index 106c6d367..76bfdb1ec 100644 --- a/src/main/lib/trpc/routers/projects.ts +++ b/src/main/lib/trpc/routers/projects.ts @@ -526,6 +526,26 @@ export const projectsRouter = router({ .get() }), + /** + * Update accent color for a project (hex string or null to clear) + */ + updateColor: publicProcedure + .input( + z.object({ + id: z.string(), + accentColor: z.string().nullable(), + }), + ) + .mutation(({ input }) => { + const db = getDatabase() + return db + .update(projects) + .set({ accentColor: input.accentColor, updatedAt: new Date() }) + .where(eq(projects.id, input.id)) + .returning() + .get() + }), + /** * Remove custom icon for a project */ diff --git a/src/main/lib/vscode-theme-scanner.ts b/src/main/lib/vscode-theme-scanner.ts index f2468e8f8..0f6d6f714 100644 --- a/src/main/lib/vscode-theme-scanner.ts +++ b/src/main/lib/vscode-theme-scanner.ts @@ -127,7 +127,7 @@ async function scanExtensionsDir(extensionsDir: string, source: EditorSource): P // Create Dirent-like objects from ls output const entries_final = await Promise.all( - lsEntries.map(async (name) => { + lsEntries.map(async (name: string) => { const fullPath = path.join(extensionsDir, name) try { const stat = await fs.stat(fullPath) diff --git a/src/main/windows/main.ts b/src/main/windows/main.ts index 15dcdd137..65f158cf3 100644 --- a/src/main/windows/main.ts +++ b/src/main/windows/main.ts @@ -81,7 +81,7 @@ function registerIpcHandlers(): void { ipcMain.handle("app:set-badge", (event, count: number | null) => { const win = getWindowFromEvent(event) if (process.platform === "darwin") { - app.dock.setBadge(count ? String(count) : "") + app.dock?.setBadge(count ? String(count) : "") } else if (process.platform === "win32" && win) { // Windows: Update title with count as fallback if (count !== null && count > 0) { diff --git a/src/renderer/components/chat-markdown-renderer.tsx b/src/renderer/components/chat-markdown-renderer.tsx index ecd899570..e8b1e6481 100644 --- a/src/renderer/components/chat-markdown-renderer.tsx +++ b/src/renderer/components/chat-markdown-renderer.tsx @@ -29,8 +29,9 @@ function escapeHtml(text: string): string { } // Code block text sizes matching paragraph text sizes +// sm uses text-[1em] so it inherits from parent fontSize (for chat font size scaling) const codeBlockTextSize = { - sm: "text-sm", + sm: "text-[1em]", md: "text-sm", lg: "text-sm", } @@ -148,6 +149,8 @@ interface ChatMarkdownRendererProps { syntaxHighlight?: boolean /** Whether content is being streamed */ isStreaming?: boolean + /** Base font size in pixels — overrides prose-sm's rem-based size so em-based styles inherit correctly */ + baseFontSize?: number } // Size-based styles inspired by Notion's spacing @@ -175,28 +178,30 @@ const sizeStyles: Record< td: string } > = { + // sm variant uses text-[1em] (parent-relative) instead of text-sm (root-relative) + // so body text inherits from the parent's fontSize — enabling chat font size scaling sm: { - h1: "text-base font-semibold text-foreground mt-[1.4em] mb-px first:mt-0 leading-[1.3]", - h2: "text-base font-semibold text-foreground mt-[1.4em] mb-px first:mt-0 leading-[1.3]", - h3: "text-sm font-semibold text-foreground mt-[1em] mb-px first:mt-0 leading-[1.3]", - h4: "text-sm font-medium text-foreground mt-[1em] mb-px first:mt-0 leading-[1.3]", - h5: "text-sm font-medium text-foreground mt-[1em] mb-px first:mt-0 leading-[1.3]", - h6: "text-sm font-medium text-foreground mt-[1em] mb-px first:mt-0 leading-[1.3]", - p: "text-sm text-foreground/80 my-px leading-normal py-[3px]", - ul: "list-disc list-inside text-sm text-foreground/80 mb-px marker:text-foreground/60", - ol: "list-decimal list-inside text-sm text-foreground/80 mb-px marker:text-foreground/60", - li: "text-sm text-foreground/80 py-[3px]", + h1: "text-[1.15em] font-semibold text-foreground mt-[1.4em] mb-px first:mt-0 leading-[1.3]", + h2: "text-[1.15em] font-semibold text-foreground mt-[1.4em] mb-px first:mt-0 leading-[1.3]", + h3: "text-[1em] font-semibold text-foreground mt-[1em] mb-px first:mt-0 leading-[1.3]", + h4: "text-[1em] font-medium text-foreground mt-[1em] mb-px first:mt-0 leading-[1.3]", + h5: "text-[1em] font-medium text-foreground mt-[1em] mb-px first:mt-0 leading-[1.3]", + h6: "text-[1em] font-medium text-foreground mt-[1em] mb-px first:mt-0 leading-[1.3]", + p: "text-[1em] text-foreground/80 my-px leading-normal py-[3px]", + ul: "list-disc list-inside text-[1em] text-foreground/80 leading-normal mb-px marker:text-foreground/60", + ol: "list-decimal list-inside text-[1em] text-foreground/80 leading-normal mb-px marker:text-foreground/60", + li: "text-[1em] text-foreground/80 leading-normal py-[3px]", inlineCode: "bg-foreground/[0.06] dark:bg-foreground/[0.1] font-mono text-[85%] rounded px-[0.4em] py-[0.2em] break-all", blockquote: - "border-l-2 border-foreground/20 pl-3 text-foreground/70 mb-px text-sm", + "border-l-2 border-foreground/20 pl-3 text-foreground/70 leading-normal mb-px text-[1em]", hr: "mt-8 mb-4 border-t border-border", - table: "w-full text-sm", + table: "w-full text-[1em]", thead: "border-b border-border", tbody: "", tr: "[&:not(:last-child)]:border-b [&:not(:last-child)]:border-border", - th: "text-left text-sm font-medium text-foreground px-3 py-2 bg-muted/50 border-r border-border last:border-r-0", - td: "text-sm text-foreground/80 px-3 py-2 border-r border-border last:border-r-0", + th: "text-left text-[1em] font-medium text-foreground px-3 py-2 bg-muted/50 border-r border-border last:border-r-0", + td: "text-[1em] text-foreground/80 px-3 py-2 border-r border-border last:border-r-0", }, md: { h1: "text-[1.5em] font-semibold text-foreground mt-[1.4em] mb-px first:mt-0 leading-[1.3]", @@ -286,6 +291,7 @@ export const ChatMarkdownRenderer = memo(function ChatMarkdownRenderer({ size = "md", className, isStreaming = false, + baseFontSize, }: ChatMarkdownRendererProps) { const codeTheme = useCodeTheme() const styles = sizeStyles[size] @@ -441,6 +447,8 @@ export const ChatMarkdownRenderer = memo(function ChatMarkdownRenderer({ "[&_table+p]:mt-4 [&_table+ul]:mt-4 [&_table+ol]:mt-4", className, )} + // Override prose-sm's rem-based font-size so em-based child styles inherit correctly + style={baseFontSize ? { fontSize: `${baseFontSize}px` } : undefined} > {blocks.map((block) => ( ))} diff --git a/src/renderer/components/dialogs/settings-tabs/agent-dialog.tsx b/src/renderer/components/dialogs/settings-tabs/agent-dialog.tsx index c618ac183..80cc95f32 100644 --- a/src/renderer/components/dialogs/settings-tabs/agent-dialog.tsx +++ b/src/renderer/components/dialogs/settings-tabs/agent-dialog.tsx @@ -13,7 +13,7 @@ interface FileAgent { tools?: string[] disallowedTools?: string[] model?: "sonnet" | "opus" | "haiku" | "inherit" - source: "user" | "project" + source: "user" | "project" | "plugin" path: string } @@ -35,7 +35,7 @@ export function AgentDialog({ open, onOpenChange, agent, onSuccess }: AgentDialo const [description, setDescription] = useState("") const [prompt, setPrompt] = useState("") const [model, setModel] = useState<"sonnet" | "opus" | "haiku" | "inherit">("inherit") - const [source, setSource] = useState<"user" | "project">("user") + const [source, setSource] = useState<"user" | "project" | "plugin">("user") const [toolMode, setToolMode] = useState("all") const [selectedTools, setSelectedTools] = useState([]) @@ -128,7 +128,7 @@ export function AgentDialog({ open, onOpenChange, agent, onSuccess }: AgentDialo tools, disallowedTools, model, - source: agent.source, + source: agent.source as "user" | "project", }) } else { createMutation.mutate({ @@ -138,7 +138,7 @@ export function AgentDialog({ open, onOpenChange, agent, onSuccess }: AgentDialo tools, disallowedTools, model, - source, + source: source as "user" | "project", }) } } diff --git a/src/renderer/components/dialogs/settings-tabs/agents-appearance-tab.tsx b/src/renderer/components/dialogs/settings-tabs/agents-appearance-tab.tsx index 0c930a45b..826deff8a 100644 --- a/src/renderer/components/dialogs/settings-tabs/agents-appearance-tab.tsx +++ b/src/renderer/components/dialogs/settings-tabs/agents-appearance-tab.tsx @@ -14,6 +14,14 @@ import { importedThemesAtom, type VSCodeFullTheme, } from "../../../lib/atoms" +import { + terminalFontSizeAtom, + type TerminalFontSize, +} from "../../../features/terminal/atoms" +import { + chatFontSizeAtom, + type ChatFontSize, +} from "../../../features/agents/atoms" import { BUILTIN_THEMES, getBuiltinThemeById, @@ -149,6 +157,12 @@ export function AgentsAppearanceTab() { // To-do list preference const [alwaysExpandTodoList, setAlwaysExpandTodoList] = useAtom(alwaysExpandTodoListAtom) + // Terminal font size + const [terminalFontSize, setTerminalFontSize] = useAtom(terminalFontSizeAtom) + + // Chat font size + const [chatFontSize, setChatFontSize] = useAtom(chatFontSizeAtom) + // VS Code themes state const [isScanning, setIsScanning] = useState(false) @@ -619,6 +633,65 @@ export function AgentsAppearanceTab() { onCheckedChange={setAlwaysExpandTodoList} /> +
+
+ + Chat font size + + + Font size for messages and responses + +
+ +
+
+
+ + Terminal font size + + + Font size for the integrated terminal + +
+ +
) diff --git a/src/renderer/components/dialogs/settings-tabs/agents-custom-agents-tab.tsx b/src/renderer/components/dialogs/settings-tabs/agents-custom-agents-tab.tsx index 349d47166..05b2aab7e 100644 --- a/src/renderer/components/dialogs/settings-tabs/agents-custom-agents-tab.tsx +++ b/src/renderer/components/dialogs/settings-tabs/agents-custom-agents-tab.tsx @@ -21,7 +21,7 @@ interface FileAgent { tools?: string[] disallowedTools?: string[] model?: "sonnet" | "opus" | "haiku" | "inherit" - source: "user" | "project" + source: "user" | "project" | "plugin" path: string } @@ -381,7 +381,7 @@ export function AgentsCustomAgentsTab() { model: data.model, tools: agent.tools, disallowedTools: agent.disallowedTools, - source: agent.source, + source: agent.source as "user" | "project", cwd: selectedProject?.path, }) toast.success("Agent saved", { description: agent.name }) diff --git a/src/renderer/components/dialogs/settings-tabs/agents-project-worktree-tab.tsx b/src/renderer/components/dialogs/settings-tabs/agents-project-worktree-tab.tsx index 079b05008..d6eed0774 100644 --- a/src/renderer/components/dialogs/settings-tabs/agents-project-worktree-tab.tsx +++ b/src/renderer/components/dialogs/settings-tabs/agents-project-worktree-tab.tsx @@ -37,6 +37,14 @@ import { cn } from "../../../lib/utils" import { ResizableSidebar } from "../../ui/resizable-sidebar" import { settingsProjectsSidebarWidthAtom } from "../../../features/agents/atoms" +// 16-color swatch palette for project accent color +const ACCENT_COLORS = [ + "#ef4444", "#f97316", "#f59e0b", "#eab308", + "#84cc16", "#22c55e", "#10b981", "#14b8a6", + "#06b6d4", "#0ea5e9", "#3b82f6", "#6366f1", + "#8b5cf6", "#a855f7", "#d946ef", "#ec4899", +] as const + // --- Detail Panel --- function ProjectDetail({ projectId }: { projectId: string }) { // Get config for selected project @@ -121,6 +129,16 @@ function ProjectDetail({ projectId }: { projectId: string }) { }, }) + // Accent color mutation + const updateColorMutation = trpc.projects.updateColor.useMutation({ + onSuccess: () => { + refetchProject() + }, + onError: (err) => { + toast.error(`Failed to update color: ${err.message}`) + }, + }) + const [showDeleteDialog, setShowDeleteDialog] = useState(false) // Project name editing @@ -410,6 +428,48 @@ function ProjectDetail({ projectId }: { projectId: string }) { + {/* ── Appearance ── */} +
+

Appearance

+
+
+
+
+ Accent Color +

Tint workspaces in the sidebar

+
+ {project?.accentColor && ( + + )} +
+
+ {ACCENT_COLORS.map((color) => ( +
+
+
+
+ {/* ── Config ── */}

Config

diff --git a/src/renderer/components/ui/resizable-sidebar.tsx b/src/renderer/components/ui/resizable-sidebar.tsx index 0afacd694..2fd0a13e3 100644 --- a/src/renderer/components/ui/resizable-sidebar.tsx +++ b/src/renderer/components/ui/resizable-sidebar.tsx @@ -368,7 +368,11 @@ export function ResizableSidebar({ }} transition={{ duration: isResizing ? 0 : animationDuration, - ease: [0.4, 0, 0.2, 1], + ease: [0.16, 1, 0.3, 1], + opacity: { + duration: isResizing ? 0 : animationDuration * 0.6, + ease: "easeOut", + }, }} className={`bg-transparent flex flex-col text-xs h-full relative ${className}`} style={{ minWidth: minWidth, overflow: "hidden", ...style }} diff --git a/src/renderer/features/agents/atoms/index.ts b/src/renderer/features/agents/atoms/index.ts index 666975a20..7df9aabb8 100644 --- a/src/renderer/features/agents/atoms/index.ts +++ b/src/renderer/features/agents/atoms/index.ts @@ -514,6 +514,14 @@ export const agentsSubChatsSidebarModeAtom = atomWithWindowStorage< "tabs" | "sidebar" >("agents-subchats-mode", "tabs", { getOnInit: true }) +// Track which workspaces are expanded in the unified sidebar tree +// Persisted per-window so each Electron window has its own expansion state +export const expandedWorkspaceIdsAtom = atomWithWindowStorage( + "agents:expandedWorkspaceIds", + [], + { getOnInit: true }, +) + // Sub-chats sidebar width (left side of chat area) export const agentsSubChatsSidebarWidthAtom = atomWithStorage( "agents-subchats-sidebar-width", @@ -1005,6 +1013,16 @@ export const workspaceDiffCacheAtomFamily = atomFamily((chatId: string) => ), ) +// Chat font size preference (persisted to localStorage) +// Controls body text size in assistant responses and user message bubbles +export type ChatFontSize = 12 | 13 | 14 | 15 | 16 +export const chatFontSizeAtom = atomWithStorage( + "preferences:chat-font-size", + 14, // Default matches previous hardcoded text-sm (14px) + undefined, + { getOnInit: true }, +) + // Show raw JSON for each message in chat (dev only) export const showMessageJsonAtom = atomWithStorage( "agents:showMessageJson", diff --git a/src/renderer/features/agents/components/agent-chat-card.tsx b/src/renderer/features/agents/components/agent-chat-card.tsx index 7f76f3cea..bfdaff04a 100644 --- a/src/renderer/features/agents/components/agent-chat-card.tsx +++ b/src/renderer/features/agents/components/agent-chat-card.tsx @@ -49,9 +49,9 @@ function GitHubAvatar({ interface AgentChatCardProps { chat: { id: string - name: string - meta: any - sandbox_id: string | null + name: string | null + meta?: any + sandbox_id?: string | null branch?: string | null } isSelected: boolean diff --git a/src/renderer/features/agents/components/agent-model-selector.tsx b/src/renderer/features/agents/components/agent-model-selector.tsx index 56cc4333c..07c4d0011 100644 --- a/src/renderer/features/agents/components/agent-model-selector.tsx +++ b/src/renderer/features/agents/components/agent-model-selector.tsx @@ -102,7 +102,7 @@ function CodexThinkingSubMenu({ const subMenuRef = useRef(null) const [showSub, setShowSub] = useState(false) const [subPos, setSubPos] = useState({ top: 0, left: 0 }) - const closeTimeout = useRef>() + const closeTimeout = useRef>(undefined) const scheduleClose = useCallback(() => { closeTimeout.current = setTimeout(() => setShowSub(false), 150) diff --git a/src/renderer/features/agents/components/agents-quick-switch-dialog.tsx b/src/renderer/features/agents/components/agents-quick-switch-dialog.tsx index 7034f8250..547d6a5a7 100644 --- a/src/renderer/features/agents/components/agents-quick-switch-dialog.tsx +++ b/src/renderer/features/agents/components/agents-quick-switch-dialog.tsx @@ -11,10 +11,10 @@ interface AgentsQuickSwitchDialogProps { isOpen: boolean chats: Array<{ id: string - name: string - meta: any - sandbox_id: string | null - updated_at: Date + name: string | null + meta?: any + sandbox_id?: string | null + updatedAt?: Date | null projectId: string }> selectedIndex: number diff --git a/src/renderer/features/agents/components/work-mode-selector.tsx b/src/renderer/features/agents/components/work-mode-selector.tsx index 2f477fc62..55e73c8b6 100644 --- a/src/renderer/features/agents/components/work-mode-selector.tsx +++ b/src/renderer/features/agents/components/work-mode-selector.tsx @@ -73,7 +73,7 @@ export function WorkModeSelector({ key={option.id} onClick={() => { if (isDisabled) return - onChange(option.id) + onChange(option.id as WorkMode) setOpen(false) }} disabled={isDisabled} diff --git a/src/renderer/features/agents/context/text-selection-context.tsx b/src/renderer/features/agents/context/text-selection-context.tsx index 6495be268..a106b0d8d 100644 --- a/src/renderer/features/agents/context/text-selection-context.tsx +++ b/src/renderer/features/agents/context/text-selection-context.tsx @@ -10,12 +10,7 @@ import { type ReactNode, } from "react" -// Chromium 137+ Selection API extension for Shadow DOM support -declare global { - interface Selection { - getComposedRanges?(options: { shadowRoots: ShadowRoot[] }): StaticRange[] - } -} +// Note: getComposedRanges is now part of the standard TypeScript DOM lib (Chromium 137+) // Discriminated union for selection source export type TextSelectionSource = diff --git a/src/renderer/features/agents/main/active-chat.tsx b/src/renderer/features/agents/main/active-chat.tsx index cf85ed178..c0765218b 100644 --- a/src/renderer/features/agents/main/active-chat.tsx +++ b/src/renderer/features/agents/main/active-chat.tsx @@ -1038,15 +1038,15 @@ interface DiffSidebarContentProps { onFileSelect: (file: { path: string }, category: string) => void chatId: string sandboxId: string | null - repository: { owner: string; name: string } | null + repository?: string diffStats: { isLoading: boolean; hasChanges: boolean; fileCount: number; additions: number; deletions: number } setDiffStats: (stats: { isLoading: boolean; hasChanges: boolean; fileCount: number; additions: number; deletions: number }) => void diffContent: string | null - parsedFileDiffs: unknown + parsedFileDiffs: ParsedDiffFile[] | null prefetchedFileContents: Record | undefined - setDiffCollapseState: (state: Map) => void - diffViewRef: React.RefObject<{ expandAll: () => void; collapseAll: () => void; getViewedCount: () => number; markAllViewed: () => void; markAllUnviewed: () => void } | null> - agentChat: { prUrl?: string; prNumber?: number } | null | undefined + setDiffCollapseState: (state: { allCollapsed: boolean; allExpanded: boolean }) => void + diffViewRef: React.RefObject + agentChat: { prUrl?: string | null; prNumber?: number | null } | null | undefined // Real-time sidebar width for responsive layout during resize sidebarWidth: number // Commit with AI @@ -1370,12 +1370,12 @@ const DiffSidebarContent = memo(function DiffSidebarContent({ void diffViewRef: React.RefObject diffSidebarRef: React.RefObject - agentChat: { prUrl?: string; prNumber?: number } | null | undefined + agentChat: { prUrl?: string | null; prNumber?: number | null } | null | undefined branchData: { current: string } | undefined gitStatus: { pushCount?: number; pullCount?: number; hasUpstream?: boolean; ahead?: number; behind?: number; staged?: any[]; unstaged?: any[]; untracked?: any[] } | undefined isGitStatusLoading: boolean @@ -1697,7 +1697,7 @@ interface DiffSidebarRendererProps { handleMarkAllViewed: () => void handleMarkAllUnviewed: () => void isDesktop: boolean - isFullscreen: boolean + isFullscreen: boolean | null setDiffDisplayMode: (mode: "side-peek" | "center-peek" | "full-page") => void handleCommitToPr: (selectedPaths?: string[]) => void isCommittingToPr: boolean @@ -1816,7 +1816,7 @@ const DiffSidebarRenderer = memo(function DiffSidebarRenderer({ onMarkAllViewed={handleMarkAllViewed} onMarkAllUnviewed={handleMarkAllUnviewed} isDesktop={isDesktop} - isFullscreen={isFullscreen} + isFullscreen={isFullscreen ?? undefined} displayMode={diffDisplayMode} onDisplayModeChange={setDiffDisplayMode} /> @@ -1838,8 +1838,8 @@ const DiffSidebarRenderer = memo(function DiffSidebarRenderer({ c.name.toLowerCase() === commandName.toLowerCase(), + (c: any) => c.name.toLowerCase() === commandName.toLowerCase(), ) if (cmd) { const { content } = await trpcClient.commands.getContent.query({ @@ -4224,7 +4229,7 @@ const ChatViewInner = memo(function ChatViewInner({ projectPath, }) const cmd = commands.find( - (c) => c.name.toLowerCase() === commandName.toLowerCase(), + (c: any) => c.name.toLowerCase() === commandName.toLowerCase(), ) if (cmd) { const { content } = await trpcClient.commands.getContent.query({ @@ -4270,7 +4275,7 @@ const ChatViewInner = memo(function ChatViewInner({ type: "data-file" as const, data: { url: f.url, - mediaType: f.mediaType, + mediaType: f.type, filename: f.filename, size: f.size, }, @@ -5104,7 +5109,8 @@ export function ChatView({ const diffContent = diffCache.diffContent // Smart setters that update the cache - const setDiffStats = useCallback((val: any) => { + type DiffStatsValue = { isLoading: boolean; hasChanges: boolean; fileCount: number; additions: number; deletions: number } + const setDiffStats = useCallback((val: DiffStatsValue | ((prev: DiffStatsValue) => DiffStatsValue)) => { setDiffCache((prev) => { const newVal = typeof val === 'function' ? val(prev.diffStats) : val // Only update if something changed @@ -7051,7 +7057,7 @@ Make sure to preserve all functionality from both branches when resolving confli notifyAgentComplete, syncFinishedMessagesToChatCache, pruneIfDetachedAndIdle, - agentChat?.isRemote, + (agentChat as any)?.isRemote, agentChat?.name, ]) @@ -7339,10 +7345,10 @@ Make sure to preserve all functionality from both branches when resolving confli .getState() .updateSubChatName(subChatIdToUpdate, name) // Also update query cache so init effect doesn't overwrite - utils.agents.getAgentChat.setData({ chatId }, (old) => { + utils.agents.getAgentChat.setData({ chatId }, (old: any) => { if (!old) return old const existsInCache = old.subChats.some( - (sc) => sc.id === subChatIdToUpdate, + (sc: any) => sc.id === subChatIdToUpdate, ) if (!existsInCache) { // Sub-chat not in cache yet (DB save still in flight) - add it @@ -7365,7 +7371,7 @@ Make sure to preserve all functionality from both branches when resolving confli } return { ...old, - subChats: old.subChats.map((sc) => + subChats: old.subChats.map((sc: any) => sc.id === subChatIdToUpdate ? { ...sc, name } : sc, ), } @@ -7373,12 +7379,11 @@ Make sure to preserve all functionality from both branches when resolving confli }, updateChatName: (chatIdToUpdate, name) => { // Optimistic update for sidebar (list query) - // On desktop, selectedTeamId is always null, so we update unconditionally utils.agents.getAgentChats.setData( { teamId: selectedTeamId }, - (old) => { + (old: any) => { if (!old) return old - return old.map((c) => + return old.map((c: any) => c.id === chatIdToUpdate ? { ...c, name } : c, ) }, @@ -7391,6 +7396,15 @@ Make sure to preserve all functionality from both branches when resolving confli return { ...old, name } }, ) + // Also directly update the tRPC cache the sidebar reads from, + // and invalidate to guarantee a refetch + trpcUtils.chats.list.setData({}, (old) => { + if (!old) return old + return old.map((c) => + c.id === chatIdToUpdate ? { ...c, name } : c, + ) + }) + trpcUtils.chats.list.invalidate() }, }) }, @@ -7402,6 +7416,7 @@ Make sure to preserve all functionality from both branches when resolving confli renameChatMutation, selectedTeamId, selectedOllamaModel, + trpcUtils.chats.list, utils.agents.getAgentChats, utils.agents.getAgentChat, ], diff --git a/src/renderer/features/agents/main/assistant-message-item.tsx b/src/renderer/features/agents/main/assistant-message-item.tsx index 08971eb8f..a530670f5 100644 --- a/src/renderer/features/agents/main/assistant-message-item.tsx +++ b/src/renderer/features/agents/main/assistant-message-item.tsx @@ -499,7 +499,7 @@ export const AssistantMessageItem = memo(function AssistantMessageItem({ // Note: no useMemo — AI SDK mutates parts in-place, so the array reference // doesn't change and useMemo would return stale results. const messageParts = normalizeAcpParts( - (message?.parts || []).map((part) => normalizeCodexToolPart(part) as any), + (message?.parts || []).map((part: any) => normalizeCodexToolPart(part) as any), ) const contentParts = useMemo(() => diff --git a/src/renderer/features/agents/main/chat-input-area.tsx b/src/renderer/features/agents/main/chat-input-area.tsx index 30ad3f25b..11caf472c 100644 --- a/src/renderer/features/agents/main/chat-input-area.tsx +++ b/src/renderer/features/agents/main/chat-input-area.tsx @@ -1155,8 +1155,7 @@ export const ChatInputArea = memo(function ChatInputArea({ // Process other files - for text files, read content and add as file mention for (const file of otherFiles) { // Get file path using Electron's webUtils API (more reliable than file.path) - // @ts-expect-error - Electron's webUtils API - const filePath: string | undefined = window.webUtils?.getPathForFile?.(file) || (file as File & { path?: string }).path + const filePath: string | undefined = (window as any).webUtils?.getPathForFile?.(file) || (file as File & { path?: string }).path let mentionId: string let mentionPath: string diff --git a/src/renderer/features/agents/main/memoized-text-part.tsx b/src/renderer/features/agents/main/memoized-text-part.tsx index 673c577e1..93faf0876 100644 --- a/src/renderer/features/agents/main/memoized-text-part.tsx +++ b/src/renderer/features/agents/main/memoized-text-part.tsx @@ -1,9 +1,11 @@ "use client" import { memo, useEffect, useRef } from "react" +import { useAtomValue } from "jotai" import { cn } from "../../../lib/utils" import { MemoizedMarkdown } from "../../../components/chat-markdown-renderer" import { useSearchQuery, useSearchHighlight } from "../search" +import { chatFontSizeAtom } from "../atoms" interface MemoizedTextPartProps { text: string @@ -101,7 +103,8 @@ const MemoizedTextPartInner = memo(function MemoizedTextPartInner({ partIndex, isFinalText, visibleStepsCount, -}: Omit) { + baseFontSize, +}: Omit & { baseFontSize?: number }) { if (!text?.trim()) return null return ( @@ -119,7 +122,7 @@ const MemoizedTextPartInner = memo(function MemoizedTextPartInner({ Response
)} - + ) }, (prev, next) => { @@ -128,7 +131,8 @@ const MemoizedTextPartInner = memo(function MemoizedTextPartInner({ prev.messageId === next.messageId && prev.partIndex === next.partIndex && prev.isFinalText === next.isFinalText && - prev.visibleStepsCount === next.visibleStepsCount + prev.visibleStepsCount === next.visibleStepsCount && + prev.baseFontSize === next.baseFontSize ) }) @@ -145,6 +149,9 @@ export const MemoizedTextPart = memo(function MemoizedTextPart({ }: MemoizedTextPartProps) { const containerRef = useRef(null) + // Chat font size preference — passed to MemoizedMarkdown to scale text via em-based styles + const chatFontSize = useAtomValue(chatFontSizeAtom) + // Search hooks - when search is closed, these return empty/null values // and don't cause re-renders (SearchHighlightProvider returns static context) const searchQuery = useSearchQuery() @@ -183,6 +190,7 @@ export const MemoizedTextPart = memo(function MemoizedTextPart({ partIndex={partIndex} isFinalText={isFinalText} visibleStepsCount={visibleStepsCount} + baseFontSize={chatFontSize} /> ) diff --git a/src/renderer/features/agents/main/new-chat-form.tsx b/src/renderer/features/agents/main/new-chat-form.tsx index 6f0f61381..c48290af1 100644 --- a/src/renderer/features/agents/main/new-chat-form.tsx +++ b/src/renderer/features/agents/main/new-chat-form.tsx @@ -712,7 +712,13 @@ export function NewChatForm({ // Fetch repos from team // Desktop: no remote repos, we use local projects - const reposData = { repositories: [] } + const reposData = { repositories: [] as Array<{ + id: string + name: string + full_name: string + sandbox_status?: "not_setup" | "in_progress" | "ready" | "error" + pushed_at?: string | null + }> } const isLoadingRepos = false // Memoize repos arrays to prevent useEffect from running on every keystroke @@ -1210,7 +1216,7 @@ export function NewChatForm({ // Create chat with selected project, branch, and initial message createChatMutation.mutate({ projectId: selectedProject.id, - name: message.trim().slice(0, 50), // Use first 50 chars as chat name + name: selectedProject.name || message.trim().slice(0, 50), // Use project name as workspace name model: selectedChatModel, initialMessageParts: parts.length > 0 ? parts : undefined, baseBranch: diff --git a/src/renderer/features/agents/mentions/agents-mentions-editor.tsx b/src/renderer/features/agents/mentions/agents-mentions-editor.tsx index 548334e42..93222c6ee 100644 --- a/src/renderer/features/agents/mentions/agents-mentions-editor.tsx +++ b/src/renderer/features/agents/mentions/agents-mentions-editor.tsx @@ -29,7 +29,7 @@ export interface FileMentionOption { description?: string // skill/agent/tool description tools?: string[] // agent allowed tools model?: string // agent model - source?: "user" | "project" // skill/agent source + source?: "user" | "project" | "plugin" // skill/agent source mcpServer?: string // MCP server name for tools } diff --git a/src/renderer/features/agents/ui/agent-diff-view.tsx b/src/renderer/features/agents/ui/agent-diff-view.tsx index 760cc5b64..91723ebb3 100644 --- a/src/renderer/features/agents/ui/agent-diff-view.tsx +++ b/src/renderer/features/agents/ui/agent-diff-view.tsx @@ -1614,8 +1614,8 @@ export const AgentDiffView = forwardRef( const newContents: Record = {} for (const [key, result] of Object.entries(results)) { - if (result.ok) { - newContents[key] = result.content + if ((result as any).ok) { + newContents[key] = (result as any).content } } setFileContents(newContents) diff --git a/src/renderer/features/agents/ui/agent-tool-registry.tsx b/src/renderer/features/agents/ui/agent-tool-registry.tsx index 3bf1cb983..822badea5 100644 --- a/src/renderer/features/agents/ui/agent-tool-registry.tsx +++ b/src/renderer/features/agents/ui/agent-tool-registry.tsx @@ -343,7 +343,7 @@ export const AgentToolRegistry: Record = { // Normalize line continuations, shorten absolute paths, and truncate let normalized = command.replace(/\\\s*\n\s*/g, " ").trim() // Replace absolute paths that look like project paths with relative versions - normalized = normalized.replace(/\/(?:Users|home|root)\/[^\s"']+/g, (match) => { + normalized = normalized.replace(/\/(?:Users|home|root)\/[^\s"']+/g, (match: string) => { return getDisplayPath(match) }) return normalized.length > 50 ? normalized.slice(0, 47) + "..." : normalized diff --git a/src/renderer/features/agents/ui/agent-user-message-bubble.tsx b/src/renderer/features/agents/ui/agent-user-message-bubble.tsx index 37938c7b3..60e72455b 100644 --- a/src/renderer/features/agents/ui/agent-user-message-bubble.tsx +++ b/src/renderer/features/agents/ui/agent-user-message-bubble.tsx @@ -1,6 +1,7 @@ "use client" import { useState, useRef, useEffect, memo, useMemo } from "react" +import { useAtomValue } from "jotai" import { cn } from "../../../lib/utils" import { useOverflowDetection } from "../../../hooks/use-overflow-detection" import { @@ -12,6 +13,7 @@ import { import { AgentImageItem } from "./agent-image-item" import { RenderFileMentions, extractTextMentions, TextMentionBlocks } from "../mentions/render-file-mentions" import { useSearchHighlight, useSearchQuery } from "../search" +import { chatFontSizeAtom } from "../atoms" interface AgentUserMessageBubbleProps { messageId: string @@ -20,6 +22,8 @@ interface AgentUserMessageBubbleProps { data?: { filename?: string url?: string + base64Data?: string + mediaType?: string } }> /** If true, renders only images and text - no TextMentionBlocks (they're rendered by parent) */ @@ -128,6 +132,9 @@ export const AgentUserMessageBubble = memo(function AgentUserMessageBubble({ // VS Code style overflow detection using ResizeObserver (no layout thrashing) const showGradient = useOverflowDetection(contentRef, [textContent]) + // Chat font size preference — applied to text bubble and expanded dialog + const chatFontSize = useAtomValue(chatFontSizeAtom) + // Search highlight support const highlights = useSearchHighlight(messageId, 0, "text") const searchQuery = useSearchQuery() @@ -228,12 +235,13 @@ export const AgentUserMessageBubble = memo(function AgentUserMessageBubble({ ref={contentRef} onClick={() => showGradient && !hasCurrentSearchHighlight && setIsExpanded(true)} className={cn( - "relative bg-input-background border px-3 py-2 rounded-xl whitespace-pre-wrap text-sm transition-all duration-200 max-h-[100px]", + "relative bg-input-background border px-3 py-2 rounded-xl whitespace-pre-wrap transition-all duration-200 max-h-[100px]", // When searching in this message, allow scroll; otherwise hide overflow hasCurrentSearchHighlight ? "overflow-y-auto" : "overflow-hidden", // Cursor and hover only when can expand (not during search) showGradient && !hasCurrentSearchHighlight && "cursor-pointer hover:brightness-110", )} + style={{ fontSize: `${chatFontSize}px` }} data-message-id={messageId} data-part-index={0} data-part-type="text" @@ -289,7 +297,7 @@ export const AgentUserMessageBubble = memo(function AgentUserMessageBubble({ {textMentions.length > 0 && ( )} -
+
diff --git a/src/renderer/features/agents/ui/agents-content.tsx b/src/renderer/features/agents/ui/agents-content.tsx index f417a2e4c..2d8a2ed42 100644 --- a/src/renderer/features/agents/ui/agents-content.tsx +++ b/src/renderer/features/agents/ui/agents-content.tsx @@ -5,11 +5,11 @@ import { useAtom, useAtomValue, useSetAtom } from "jotai" import { useQuery } from "@tanstack/react-query" // import { useSearchParams, useRouter } from "next/navigation" // Desktop doesn't use next/navigation // Desktop: mock Next.js navigation hooks -const useSearchParams = () => ({ get: () => null }) -const useRouter = () => ({ push: () => {}, replace: () => {} }) +const useSearchParams = () => ({ get: (_key: string) => null }) +const useRouter = () => ({ push: (_url: string) => {}, replace: (_url: string, _opts?: any) => {} }) // Desktop: mock Clerk hooks const useUser = () => ({ user: null }) -const useClerk = () => ({ signOut: () => {} }) +const useClerk = () => ({ signOut: (_opts?: any) => {} }) import { selectedAgentChatIdAtom, selectedChatIsRemoteAtom, @@ -19,8 +19,6 @@ import { agentsMobileViewModeAtom, agentsPreviewSidebarOpenAtom, agentsSidebarOpenAtom, - agentsSubChatsSidebarModeAtom, - agentsSubChatsSidebarWidthAtom, desktopViewAtom, } from "../atoms" import { @@ -46,7 +44,7 @@ import { api } from "../../../lib/mock-api" import { trpc } from "../../../lib/trpc" import { useIsMobile } from "../../../lib/hooks/use-mobile" import { AgentsSidebar } from "../../sidebar/agents-sidebar" -import { AgentsSubChatsSidebar } from "../../sidebar/agents-subchats-sidebar" +// AgentsSubChatsSidebar removed — unified sidebar handles sub-chats now import { AgentPreview } from "./agent-preview" import { AgentDiffView } from "./agent-diff-view" import { TerminalSidebar, terminalSidebarOpenAtomFamily } from "../../terminal" @@ -57,8 +55,7 @@ import { } from "../stores/sub-chat-store" import { useShallow } from "zustand/react/shallow" import { motion, AnimatePresence } from "motion/react" -// import { ResizableSidebar } from "@/app/(alpha)/canvas/[id]/{components}/resizable-sidebar" -import { ResizableSidebar } from "../../../components/ui/resizable-sidebar" +// ResizableSidebar removed — sub-chats sidebar no longer rendered here // import { useClerk, useUser } from "@clerk/nextjs" // import { useCombinedAuth } from "@/lib/hooks/use-combined-auth" const useCombinedAuth = () => ({ userId: null }) // Desktop mock @@ -95,9 +92,6 @@ export function AgentsContent() { agentsPreviewSidebarOpenAtom, ) const [mobileViewMode, setMobileViewMode] = useAtom(agentsMobileViewModeAtom) - const [subChatsSidebarMode, setSubChatsSidebarMode] = useAtom( - agentsSubChatsSidebarModeAtom, - ) // Per-chat terminal sidebar state const terminalSidebarAtom = useMemo( () => terminalSidebarOpenAtomFamily(selectedChatId || ""), @@ -105,10 +99,7 @@ export function AgentsContent() { ) const setTerminalSidebarOpen = useSetAtom(terminalSidebarAtom) - const hasOpenedSubChatsSidebar = useRef(false) - const wasSubChatsSidebarOpen = useRef(false) - const [shouldAnimateSubChatsSidebar, setShouldAnimateSubChatsSidebar] = - useState(subChatsSidebarMode !== "sidebar") + // Sub-chats sidebar refs removed — unified sidebar handles sub-chats now const searchParams = useSearchParams() const router = useRouter() const isInitialized = useRef(false) @@ -321,7 +312,7 @@ export function AgentsContent() { const sortedChats = agentChats ? [...agentChats].sort( (a, b) => - new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(), + new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime(), ) : [] @@ -460,8 +451,8 @@ export function AgentsContent() { // Get sorted chat list const sortedChats = [...agentChats].sort( (a, b) => - new Date(b.updated_at).getTime() - - new Date(a.updated_at).getTime(), + new Date(b.updatedAt ?? 0).getTime() - + new Date(a.updatedAt ?? 0).getTime(), ) isNavigatingRef.current = true setTimeout(() => { @@ -782,44 +773,7 @@ export function AgentsContent() { } } - // Check if sub-chats data is loaded (use separate selectors to avoid object creation) - const subChatsStoreChatId = useAgentSubChatStore((state) => state.chatId) - const subChatsCount = useAgentSubChatStore( - (state) => state.allSubChats.length, - ) - - // Check if sub-chats are still loading (store not yet initialized for this chat) - const isLoadingSubChats = - selectedChatId !== null && - (subChatsStoreChatId !== selectedChatId || subChatsCount === 0) - - // Track sub-chats sidebar open state for animation control - // Now renders even while loading to show spinner (mobile always uses tabs) - const isSubChatsSidebarOpen = - selectedChatId && - subChatsSidebarMode === "sidebar" && - !isMobile && - !desktopView - - useEffect(() => { - // When sidebar closes, reset for animation on next open - if (!isSubChatsSidebarOpen && wasSubChatsSidebarOpen.current) { - hasOpenedSubChatsSidebar.current = false - setShouldAnimateSubChatsSidebar(true) - } - wasSubChatsSidebarOpen.current = !!isSubChatsSidebarOpen - - // Mark as opened after animation completes - if (isSubChatsSidebarOpen && !hasOpenedSubChatsSidebar.current) { - const timer = setTimeout(() => { - hasOpenedSubChatsSidebar.current = true - setShouldAnimateSubChatsSidebar(false) - }, 150 + 50) // 150ms duration + 50ms buffer - return () => clearTimeout(timer) - } else if (isSubChatsSidebarOpen && hasOpenedSubChatsSidebar.current) { - setShouldAnimateSubChatsSidebar(false) - } - }, [isSubChatsSidebarOpen]) + // Sub-chats sidebar removed — unified sidebar handles hierarchy now // Check if chat has sandbox with port for preview const chatMeta = chatData?.meta as @@ -966,35 +920,6 @@ export function AgentsContent() { return ( <>
- {/* Sub-chats sidebar - only show in sidebar mode when viewing a chat */} - { - setShouldAnimateSubChatsSidebar(true) - setSubChatsSidebarMode("tabs") - }} - widthAtom={agentsSubChatsSidebarWidthAtom} - minWidth={160} - maxWidth={300} - side="left" - animationDuration={0} - initialWidth={0} - exitWidth={0} - disableClickToClose={true} - > - { - setShouldAnimateSubChatsSidebar(true) - setSubChatsSidebarMode("tabs") - }} - isMobile={isMobile} - isSidebarOpen={sidebarOpen} - onBackToChats={() => setSidebarOpen((prev) => !prev)} - isLoading={isLoadingSubChats} - agentName={chatData?.name} - /> - - {/* Main content */}
void hasUnseenChanges?: boolean + /** @deprecated Sub-chats sidebar removed — unified sidebar handles hierarchy now */ isSubChatsSidebarOpen?: boolean } @@ -22,36 +12,8 @@ export function AgentsHeaderControls({ isSidebarOpen, onToggleSidebar, hasUnseenChanges = false, - isSubChatsSidebarOpen = false, }: AgentsHeaderControlsProps) { - const toggleSidebarHotkey = useResolvedHotkeyDisplay("toggle-sidebar") - - // Only show open button when both sidebars are closed - if (isSidebarOpen || isSubChatsSidebarOpen) return null - - return ( - - - - - - - Open sidebar - {toggleSidebarHotkey && {toggleSidebarHotkey}} - - - - ) + // Sidebar toggle is now handled by the floating button in agents-layout.tsx + // Keeping this component to avoid breaking imports across the codebase + return null } diff --git a/src/renderer/features/agents/ui/mcp-servers-indicator.tsx b/src/renderer/features/agents/ui/mcp-servers-indicator.tsx index 23bfd9ae1..108c875f4 100644 --- a/src/renderer/features/agents/ui/mcp-servers-indicator.tsx +++ b/src/renderer/features/agents/ui/mcp-servers-indicator.tsx @@ -53,7 +53,7 @@ export const McpServersIndicator = memo(function McpServersIndicator({ tools: prev?.tools || [], mcpServers: mcpConfig.mcpServers.map((s) => ({ name: s.name, - status: s.status, + status: s.status as MCPServerStatus, })), plugins: prev?.plugins || [], skills: prev?.skills || [], diff --git a/src/renderer/features/agents/ui/sub-chat-selector.tsx b/src/renderer/features/agents/ui/sub-chat-selector.tsx index b890510da..e2caa899a 100644 --- a/src/renderer/features/agents/ui/sub-chat-selector.tsx +++ b/src/renderer/features/agents/ui/sub-chat-selector.tsx @@ -19,7 +19,6 @@ import { IconSpinner, PlanIcon, AgentIcon, - IconOpenSidebarRight, PinFilledIcon, DiffIcon, ClockIcon, @@ -656,27 +655,6 @@ export function SubChatSelector({ )} - {/* Open sidebar button - only on desktop when in tabs mode */} - {!isMobile && subChatsSidebarMode === "tabs" && ( - - - - - Open chats pane - - )} -
1 && editingSubChatId !== subChat.id && ( -
+
state.setChatId) @@ -306,6 +312,50 @@ export function AgentsLayout() {
{/* Windows Title Bar (only shown on Windows with frameless window) */} + + {/* Persistent title bar controls — always visible regardless of sidebar state */} + {isDesktop && !isFullscreen && !isSettingsView && ( +
+ {/* Drag region covering title bar area */} +
+ + {/* No-drag zone over native traffic lights */} + + + {/* Sidebar toggle — same icon & position whether open or closed */} +
+ +
+
+ )} +
{/* Left Sidebar - switches between chat list and settings nav */} {isSettingsView ? ( ) : ( @@ -335,7 +385,11 @@ export function AgentsLayout() { {/* Main Content */} -
+
+ {/* Spacer for traffic lights when sidebar is closed */} + {isDesktop && !isFullscreen && !sidebarOpen && !isSettingsView && ( + + )}
diff --git a/src/renderer/features/mentions/providers/agents-provider.ts b/src/renderer/features/mentions/providers/agents-provider.ts index 98ce1a6d9..f0c2c1625 100644 --- a/src/renderer/features/mentions/providers/agents-provider.ts +++ b/src/renderer/features/mentions/providers/agents-provider.ts @@ -30,7 +30,7 @@ export interface AgentData { tools?: string[] disallowedTools?: string[] model?: AgentModel - source: "user" | "project" + source: "user" | "project" | "plugin" path: string } @@ -66,7 +66,7 @@ export const agentsProvider = createMentionProvider({ }) // Map to MentionItem format - let items: MentionItem[] = agents.map((agent) => ({ + let items: MentionItem[] = agents.map((agent: any) => ({ id: `${MENTION_PREFIXES.AGENT}${agent.name}`, label: agent.name, description: agent.description || "", diff --git a/src/renderer/features/mentions/providers/files-provider.ts b/src/renderer/features/mentions/providers/files-provider.ts index 18e435251..bb44e0d2e 100644 --- a/src/renderer/features/mentions/providers/files-provider.ts +++ b/src/renderer/features/mentions/providers/files-provider.ts @@ -80,7 +80,7 @@ export const filesProvider = createMentionProvider({ }) // Map to MentionItem format - const items: MentionItem[] = results.map((result) => ({ + const items: MentionItem[] = results.map((result: any) => ({ id: result.id, label: result.label, description: result.path, diff --git a/src/renderer/features/mentions/providers/skills-provider.ts b/src/renderer/features/mentions/providers/skills-provider.ts index 7c6a2fbc5..a90307317 100644 --- a/src/renderer/features/mentions/providers/skills-provider.ts +++ b/src/renderer/features/mentions/providers/skills-provider.ts @@ -21,7 +21,7 @@ import { export interface SkillData { name: string description: string - source: "user" | "project" + source: "user" | "project" | "plugin" path: string } @@ -57,7 +57,7 @@ export const skillsProvider = createMentionProvider({ }) // Map to MentionItem format - let items: MentionItem[] = skills.map((skill) => ({ + let items: MentionItem[] = skills.map((skill: any) => ({ id: `${MENTION_PREFIXES.SKILL}${skill.name}`, label: skill.name, description: skill.description || skill.path, diff --git a/src/renderer/features/sidebar/agents-sidebar.tsx b/src/renderer/features/sidebar/agents-sidebar.tsx index feb803843..02738b880 100644 --- a/src/renderer/features/sidebar/agents-sidebar.tsx +++ b/src/renderer/features/sidebar/agents-sidebar.tsx @@ -11,6 +11,7 @@ import { autoAdvanceTargetAtom, createTeamDialogOpenAtom, agentsSettingsDialogActiveTabAtom, + type SettingsTab, agentsSidebarOpenAtom, agentsHelpPopoverOpenAtom, selectedAgentChatIdsAtom, @@ -41,13 +42,15 @@ import { import { usePrefetchLocalChat } from "../../lib/hooks/use-prefetch-local-chat" import { ArchivePopover } from "../agents/ui/archive-popover" import { ChevronDown, MoreHorizontal, Columns3, ArrowUpRight } from "lucide-react" +import { IconChevronRight, IconChevronDown, IconChevronUp, IconArchive, IconPlus, IconFolder, IconFolderOpen, IconSortDescending, IconSettings, IconX, IconSparkles, IconEdit, IconFolderPlus, IconSearch, IconArrowsDiagonalMinimize2, IconDots, IconPointFilled, IconLogin, IconLayoutSidebarLeftCollapse, IconFilter, IconLayoutGrid } from "@tabler/icons-react" +import { Skeleton } from "../../components/ui/skeleton" import { useQuery } from "@tanstack/react-query" import { remoteTrpc } from "../../lib/remote-trpc" // import { useRouter } from "next/navigation" // Desktop doesn't use next/navigation // import { useCombinedAuth } from "@/lib/hooks/use-combined-auth" -const useCombinedAuth = () => ({ userId: null }) +const useCombinedAuth = () => ({ userId: null, isLoaded: true }) // import { AuthDialog } from "@/components/auth/auth-dialog" -const AuthDialog = () => null +const AuthDialog = (_props: { open?: boolean; onOpenChange?: (open: boolean) => void }) => null // Desktop: archive is handled inline, not via hook // import { DiscordIcon } from "@/components/icons" import { DiscordIcon } from "../../icons" @@ -55,7 +58,7 @@ import { AgentsRenameSubChatDialog } from "../agents/components/agents-rename-su import { OpenLocallyDialog } from "../agents/components/open-locally-dialog" import { useAutoImport } from "../agents/hooks/use-auto-import" import { ConfirmArchiveDialog } from "../../components/confirm-archive-dialog" -import { trpc } from "../../lib/trpc" +import { trpc, trpcClient } from "../../lib/trpc" import { toast } from "sonner" import { DropdownMenu, @@ -72,6 +75,7 @@ import { TooltipContent, TooltipTrigger, } from "../../components/ui/tooltip" +// Popover imports removed — workspace settings now navigates to project settings page import { Kbd } from "../../components/ui/kbd" import { ContextMenu, @@ -86,13 +90,11 @@ import { import { IconDoubleChevronLeft, SettingsIcon, - PlusIcon, ProfileIcon, PublisherStudioIcon, SearchIcon, GitHubLogo, LoadingDot, - ArchiveIcon, TrashIcon, QuestionCircleIcon, QuestionIcon, @@ -111,6 +113,7 @@ import { showNewChatFormAtom, loadingSubChatsAtom, agentsUnseenChangesAtom, + agentsSubChatUnseenChangesAtom, archivePopoverOpenAtom, agentsDebugModeAtom, selectedProjectAtom, @@ -118,15 +121,18 @@ import { undoStackAtom, pendingUserQuestionsAtom, desktopViewAtom, + expandedWorkspaceIdsAtom, + subChatFilesAtom, type UndoItem, } from "../agents/atoms" import { NetworkStatus } from "../../components/ui/network-status" -import { useAgentSubChatStore, OPEN_SUB_CHATS_CHANGE_EVENT } from "../agents/stores/sub-chat-store" +import { useAgentSubChatStore, OPEN_SUB_CHATS_CHANGE_EVENT, type SubChatMeta } from "../agents/stores/sub-chat-store" import { getWindowId } from "../../contexts/WindowContext" import { AgentsHelpPopover } from "../agents/components/agents-help-popover" import { getShortcutKey, isDesktopApp } from "../../lib/utils/platform" import { useResolvedHotkeyDisplay, useResolvedHotkeyDisplayWithAlt } from "../../lib/hotkeys" import { pluralize } from "../agents/utils/pluralize" +import { formatTimeAgo } from "../agents/utils/format-time-ago" import { useNewChatDrafts, deleteNewChatDraft, type NewChatDraft } from "../agents/lib/drafts" import { TrafficLightSpacer, @@ -156,20 +162,23 @@ const GitHubAvatar = React.memo(function GitHubAvatar({ const handleLoad = useCallback(() => setIsLoaded(true), []) const handleError = useCallback(() => setHasError(true), []) + // Detect if parent wants rounded-full (circle) style + const isCircle = className?.includes("rounded-full") + if (hasError) { return } return ( -
+
{/* Placeholder background while loading */} {!isLoaded && ( -
+
)} {gitOwner} @@ -354,68 +363,103 @@ const DraftItem = React.memo(function DraftItem({
onSelect(draftId)} className={cn( - "w-full text-left py-1.5 cursor-pointer group relative", - "transition-colors duration-75", + "w-full text-left py-[7px] cursor-pointer group relative", + "transition-colors duration-150 rounded-lg", "outline-offset-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70", - isMultiSelectMode ? "px-3" : "pl-2 pr-2", - !isMultiSelectMode && "rounded-md", + isMultiSelectMode ? "px-3" : "pl-[22px] pr-2", isSelected - ? "bg-foreground/5 text-foreground" - : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground", + ? "bg-foreground/[0.08] text-foreground" + : "text-muted-foreground/60 hover:bg-foreground/[0.04] hover:text-foreground", )} > -
- {showIcon && ( -
-
- {projectGitOwner && projectGitProvider === "github" ? ( - - ) : ( - - )} -
-
- )} -
-
- - {draftText.slice(0, 50)} - {draftText.length > 50 ? "..." : ""} - - {/* Delete button - shown on hover */} - {!isMultiSelectMode && !isMobileFullscreen && ( - - )} -
-
- - Draft - {projectGitRepo - ? ` • ${projectGitRepo}` - : projectName - ? ` • ${projectName}` - : ""} - - - {formatTime(new Date(draftUpdatedAt).toISOString())} - -
+
+ {/* Draft indicator dot */} +
+
+
+
+ + {draftText.slice(0, 50)} + {draftText.length > 50 ? "..." : ""} + + {/* Delete button on hover */} + {!isMultiSelectMode && !isMobileFullscreen && ( + + )}
) }) +// ── Grid Pulse Spinner ───────────────────────────────────────────────────── +// A 2x2 grid of dots that pulse in staggered sequence — used as the loading +// indicator for active sub-chat threads. Much more visually appealing than +// a simple spinning circle at small sizes. +const gridDotVariants = { + idle: { opacity: 0.15, scale: 0.8 }, + pulse: { + opacity: [0.15, 1, 0.15], + scale: [0.8, 1.15, 0.8], + transition: { + duration: 1.4, + repeat: Infinity, + ease: "easeInOut", + }, + }, + // Paused state — dots visible at rest, no animation + paused: { + opacity: 0.6, + scale: 1, + transition: { duration: 0.3, ease: "easeOut" }, + }, +} + +const GridPulseSpinner = React.memo(function GridPulseSpinner({ + size = 10, + className, + paused = false, +}: { + size?: number + className?: string + paused?: boolean +}) { + // Each dot is ~38% of container to leave gaps + const dotSize = Math.max(1, Math.round(size * 0.38)) + const gap = Math.max(1, Math.round(size * 0.12)) + + return ( + + {[0, 1, 2, 3].map((i) => ( + + ))} + + ) +}) + // Memoized Agent Chat Item component to prevent re-renders on hover const AgentChatItem = React.memo(function AgentChatItem({ chatId, @@ -465,6 +509,11 @@ const AgentChatItem = React.memo(function AgentChatItem({ nameRefCallback, formatTime, isJustCreated, + onCreateSubChat, + accentColor, + onNavigateToSettings, + isExpanded, + onToggleExpand, }: { chatId: string chatName: string | null @@ -513,6 +562,11 @@ const AgentChatItem = React.memo(function AgentChatItem({ nameRefCallback: (chatId: string, el: HTMLSpanElement | null) => void formatTime: (dateStr: string) => string isJustCreated: boolean + onCreateSubChat?: (chatId: string) => void + accentColor?: string | null + onNavigateToSettings?: (chatProjectId: string) => void + isExpanded?: boolean + onToggleExpand?: () => void }) { // Resolved hotkey for context menu const archiveWorkspaceHotkey = useResolvedHotkeyDisplay("archive-workspace") @@ -547,150 +601,131 @@ const AgentChatItem = React.memo(function AgentChatItem({ onMouseEnter(chatId, chatName, e.currentTarget, globalIndex) }} onMouseLeave={onMouseLeave} + style={accentColor ? { + borderLeftColor: accentColor, + backgroundColor: `${accentColor}0a`, // ~4% opacity tint + } : undefined} className={cn( - "w-full text-left py-1.5 cursor-pointer group relative", - "transition-colors duration-75", + "w-full text-left py-1 cursor-pointer group relative", + "transition-[background-color,color,border-color] duration-150 ease-out", "outline-offset-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70", - // In multi-select: px-3 compensates for removed container px-2, keeping text aligned - isMultiSelectMode ? "px-3" : "pl-2 pr-2", - !isMultiSelectMode && "rounded-md", - isSelected - ? "bg-foreground/5 text-foreground" - : isFocused - ? "bg-foreground/5 text-foreground" - : // On mobile, no hover effect to prevent double-tap issue - isMobileFullscreen - ? "text-muted-foreground" - : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground", + // Accent color left border when set + accentColor ? "border-l-2 rounded-r-md" : "", + isMultiSelectMode ? "px-3" : "pl-0.5 pr-1", isChecked && (isMobileFullscreen - ? "bg-primary/10" - : "bg-primary/10 hover:bg-primary/15"), + ? "bg-primary/10 rounded-lg" + : "bg-primary/10 hover:bg-primary/15 rounded-lg"), )} > -
- {/* Icon container - only render if showIcon or in multi-select mode */} - {(showIcon || isMultiSelectMode) && ( -
- onCheckboxClick(e, chatId)} - gitOwner={gitOwner} - gitProvider={gitProvider} - showIcon={showIcon} +
+ {/* Multi-select checkbox or folder icon */} + {isMultiSelectMode ? ( +
onCheckboxClick(e, chatId)}> +
- )} -
-
- nameRefCallback(chatId, el)} - className="truncate block text-sm leading-tight flex-1" - > - - - {/* Archive button or inline loader/status when icon is hidden */} - {!isMultiSelectMode && !isMobileFullscreen && ( -
- {/* Inline loader/status when icon is hidden - always visible, hides on hover */} - {!showIcon && (hasPendingQuestion || isLoading || hasUnseenChanges || hasPendingPlan) && ( -
- - {hasPendingQuestion ? ( - - - - ) : isLoading ? ( - - - - ) : hasPendingPlan ? ( - - ) : ( - - - - )} - -
- )} - {/* Archive button - appears on hover */} - -
- )} -
-
- {/* Cloud icon for remote chats */} - {isRemote && ( - - )} - {displayText} -
- {stats && (stats.additions > 0 || stats.deletions > 0) && ( - <> - - +{stats.additions} - - - -{stats.deletions} - - + ) : ( +
{ + e.stopPropagation() + onToggleExpand?.() + }} + > + {/* Default icon (avatar or folder); chevron replaces it on hover */} +
+ {gitOwner && gitProvider === "github" ? ( + + ) : isExpanded ? ( + + ) : ( + + )} +
+ {/* Chevron on hover — up when expanded, down when collapsed */} +
+ {isExpanded ? ( + + ) : ( + )} - - {formatTime( - chatUpdatedAt?.toISOString() ?? new Date().toISOString(), - )} -
+ {/* Status badge — only show for pending questions (Codex-minimal) */} + {hasPendingQuestion && ( +
+ )}
+ )} + {/* Workspace name — Codex style, no subtitle */} +
+ nameRefCallback(chatId, el)} + className={cn( + "truncate block text-[13px] leading-snug", + isSelected ? "text-foreground font-medium" : "text-muted-foreground/80 group-hover:text-foreground", + )} + > + +
+ {/* Workspace hover actions — Codex style: three dots + new thread */} + {!isMultiSelectMode && !isMobileFullscreen && ( +
+ + {onCreateSubChat && ( + + )} +
+ )}
@@ -796,6 +831,392 @@ const AgentChatItem = React.memo(function AgentChatItem({ ) }) +// Memoized Sub-Chat Item - renders an indented sub-chat row within a workspace group +const SubChatItem = React.memo(function SubChatItem({ + subChat, + isActive, + isLoading, + hasUnseenChanges, + onSelect, + onArchive, + accentColor, + additions, + deletions, + updatedAt, +}: { + subChat: SubChatMeta + isActive: boolean + isLoading: boolean + hasUnseenChanges: boolean + onSelect: (subChat: SubChatMeta) => void + onArchive: (subChatId: string) => void + accentColor?: string | null + additions?: number + deletions?: number + updatedAt?: string +}) { + // Show metadata line if we have file stats or a timestamp + const hasStats = (additions ?? 0) > 0 || (deletions ?? 0) > 0 + const hasMetadata = hasStats || !!updatedAt + + return ( +
onSelect(subChat)} + style={accentColor ? { + borderLeftColor: accentColor, + backgroundColor: isActive ? `${accentColor}12` : undefined, // Stronger tint when active + } : undefined} + className={cn( + "w-full text-left py-[6px] pl-[20px] pr-2 cursor-pointer group/subchat relative", + "transition-[background-color,color,border-color,opacity,transform] duration-150 ease-out rounded-md", + // Accent color left border for visual grouping + accentColor ? "border-l-2 rounded-l-none" : "", + isActive + ? accentColor ? "text-foreground" : "bg-foreground/[0.08] text-foreground" + : "text-muted-foreground/70 hover:bg-foreground/[0.05] hover:text-foreground", + )} + > +
+ {/* Thread status indicator — always visible: animated when loading, paused when idle */} +
+ {hasUnseenChanges ? ( +
+ ) : ( + + )} +
+ + {subChat.name || "New Chat"} + + {/* Time ago — Codex style, right-aligned, fades out on hover */} + {updatedAt && ( + + {formatTimeAgo(updatedAt)} + + )} + {/* Archive button on hover — slides in from right */} + +
+
+ ) +}) + +// ── Confirm Thread Archive Dialog ──────────────────────────────────────── +// Lightweight confirmation modal before deleting a sub-chat thread +const ConfirmThreadArchiveDialog = React.memo(function ConfirmThreadArchiveDialog({ + isOpen, + threadName, + onClose, + onConfirm, + isPending, +}: { + isOpen: boolean + threadName: string + onClose: () => void + onConfirm: () => void + isPending: boolean +}) { + const [mounted, setMounted] = useState(false) + const openAtRef = useRef(0) + const confirmRef = useRef(null) + + useEffect(() => { setMounted(true) }, []) + + useEffect(() => { + if (isOpen) openAtRef.current = performance.now() + }, [isOpen]) + + // Auto-focus confirm button when dialog opens + const handleAnimationComplete = useCallback(() => { + if (isOpen) confirmRef.current?.focus() + }, [isOpen]) + + // Prevent accidental immediate clicks (250ms grace period) + const canInteract = useCallback(() => { + return performance.now() - openAtRef.current > 250 + }, []) + + const handleClose = useCallback(() => { + if (!canInteract()) return + onClose() + }, [canInteract, onClose]) + + const handleConfirm = useCallback(() => { + if (!canInteract()) return + onConfirm() + }, [canInteract, onConfirm]) + + // Keyboard: Escape to close, Enter to confirm + useEffect(() => { + if (!isOpen) return + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { e.preventDefault(); handleClose() } + if (e.key === "Enter") { e.preventDefault(); handleConfirm() } + } + document.addEventListener("keydown", handleKeyDown) + return () => document.removeEventListener("keydown", handleKeyDown) + }, [isOpen, handleClose, handleConfirm]) + + if (!mounted) return null + const portalTarget = typeof document !== "undefined" ? document.body : null + if (!portalTarget) return null + + const EASING = [0.55, 0.055, 0.675, 0.19] as const + + return createPortal( + + {isOpen && ( + <> + {/* Overlay */} + + {/* Dialog */} +
+ e.stopPropagation()} + > +
+
+

Archive Thread

+

+ Are you sure you want to archive{" "} + + {threadName || "this thread"} + + ? This will permanently remove it and its messages. +

+
+
+ + Cancel + + + {isPending ? "Archiving..." : "Archive"} + +
+
+
+
+ + )} +
, + portalTarget, + ) +}) + +// Renders the sub-chat list for an expanded workspace +const WorkspaceSubChats = React.memo(function WorkspaceSubChats({ + chatId, + isRemote, + searchQuery, + onSubChatSelect, + accentColor, +}: { + chatId: string + isRemote: boolean + searchQuery?: string + onSubChatSelect: (workspaceId: string, subChat: SubChatMeta, isRemote: boolean) => void + accentColor?: string | null +}) { + // Fetch sub-chats from tRPC for this workspace + const { data: chatData, isLoading: isLoadingChatData } = trpc.chats.get.useQuery( + { id: chatId }, + { enabled: !isRemote }, // Only fetch for local chats + ) + + const utils = trpc.useUtils() + const loadingSubChats = useAtomValue(loadingSubChatsAtom) + const unseenChanges = useAtomValue(agentsSubChatUnseenChangesAtom) + const subChatFiles = useAtomValue(subChatFilesAtom) + const activeSubChatId = useAgentSubChatStore((state) => state.activeSubChatId) + const selectedChatId = useAtomValue(selectedAgentChatIdAtom) + + // Confirmation dialog state for thread archive + const [archiveConfirmId, setArchiveConfirmId] = useState(null) + const archiveConfirmName = useMemo(() => { + if (!archiveConfirmId || !chatData?.subChats) return "" + return chatData.subChats.find((sc) => sc.id === archiveConfirmId)?.name ?? "Untitled" + }, [archiveConfirmId, chatData?.subChats]) + + // Delete sub-chat mutation — actually removes from the database + const deleteSubChatMutation = trpc.chats.deleteSubChat.useMutation({ + onSuccess: () => { + if (archiveConfirmId) { + // Remove from Zustand open tabs + allSubChats + useAgentSubChatStore.getState().removeFromOpenSubChats(archiveConfirmId) + // Invalidate the workspace query so the list refreshes + utils.chats.get.invalidate({ id: chatId }) + } + setArchiveConfirmId(null) + }, + onError: () => { + toast.error("Failed to archive thread") + setArchiveConfirmId(null) + }, + }) + + // Sort sub-chats by most recent first, then filter by search query + const subChats = useMemo(() => { + if (!chatData?.subChats) return [] + const sorted = [...chatData.subChats].sort((a, b) => { + const aT = new Date(a.updatedAt || a.createdAt || "0").getTime() + const bT = new Date(b.updatedAt || b.createdAt || "0").getTime() + return bT - aT + }) + // Apply search filter if provided + if (searchQuery?.trim()) { + const query = searchQuery.toLowerCase() + return sorted.filter((sc) => + (sc.name ?? "").toLowerCase().includes(query), + ) + } + return sorted + }, [chatData?.subChats, searchQuery]) + + // Show confirmation dialog before archiving + const handleArchiveSubChat = useCallback((subChatId: string) => { + setArchiveConfirmId(subChatId) + }, []) + + // Confirm the archive — actually delete the sub-chat + const handleConfirmArchive = useCallback(() => { + if (!archiveConfirmId) return + deleteSubChatMutation.mutate({ id: archiveConfirmId }) + }, [archiveConfirmId, deleteSubChatMutation]) + + // Skeleton loading rows while fetching sub-chats + if (isLoadingChatData && !chatData) { + return ( +
+ {[1, 2].map((i) => ( +
+ +
+ ))} +
+ ) + } + + if (!chatData?.subChats || chatData.subChats.length === 0) { + return ( +
+ No threads +
+ ) + } + + // All sub-chats filtered out by search + if (subChats.length === 0) { + return null + } + + return ( + <> + + {subChats.map((sc) => { + // Compute file change stats from subChatFilesAtom + const fileChanges = subChatFiles.get(sc.id) || [] + const stats = fileChanges.length > 0 + ? fileChanges.reduce( + (acc, f) => ({ additions: acc.additions + f.additions, deletions: acc.deletions + f.deletions }), + { additions: 0, deletions: 0 }, + ) + : null + + return ( + + onSubChatSelect(chatId, subChat, isRemote)} + onArchive={handleArchiveSubChat} + accentColor={accentColor} + additions={stats?.additions} + deletions={stats?.deletions} + updatedAt={sc.updatedAt?.toISOString() ?? undefined} + /> + + ) + })} + + + {/* Thread archive confirmation dialog */} + setArchiveConfirmId(null)} + onConfirm={handleConfirmArchive} + isPending={deleteSubChatMutation.isPending} + /> + + ) +}) + // Custom comparator for ChatListSection to handle Set/Map props correctly // Sets and Maps from Jotai atoms are stable by reference when unchanged, // but we add explicit size checks for extra safety @@ -816,6 +1237,7 @@ function chatListSectionPropsAreEqual( if (prevProps.isMobileFullscreen !== nextProps.isMobileFullscreen) return false if (prevProps.isDesktop !== nextProps.isDesktop) return false if (prevProps.showIcon !== nextProps.showIcon) return false + if (prevProps.showHeader !== nextProps.showHeader) return false // Check arrays by reference (they're stable from useMemo in parent) if (prevProps.chats !== nextProps.chats) return false @@ -834,6 +1256,11 @@ function chatListSectionPropsAreEqual( if (prevProps.projectsMap !== nextProps.projectsMap) return false if (prevProps.workspaceFileStats !== nextProps.workspaceFileStats) return false + // Check hierarchical expand/collapse props by reference + if (prevProps.expandedSet !== nextProps.expandedSet) return false + if (prevProps.searchQuery !== nextProps.searchQuery) return false + if (prevProps.sortMode !== nextProps.sortMode) return false + // Callback functions are stable from useCallback in parent // No need to compare them - they only change when their deps change @@ -842,12 +1269,14 @@ function chatListSectionPropsAreEqual( interface ChatListSectionProps { title: string + showHeader?: boolean chats: Array<{ id: string name: string | null branch: string | null updatedAt: Date | null projectId: string | null + accentColor?: string | null isRemote: boolean meta?: { repository?: string; branch?: string | null } | null remoteStats?: { fileCount: number; additions: number; deletions: number } | null @@ -864,7 +1293,7 @@ interface ChatListSectionProps { isMobileFullscreen: boolean isDesktop: boolean pinnedChatIds: Set - projectsMap: Map + projectsMap: Map workspaceFileStats: Map filteredChats: Array<{ id: string }> canShowPinOption: boolean @@ -889,6 +1318,18 @@ interface ChatListSectionProps { nameRefCallback: (chatId: string, el: HTMLSpanElement | null) => void formatTime: (dateStr: string) => string justCreatedIds: Set + // Hierarchical expand/collapse props + expandedSet: Set + onToggleExpand: (chatId: string) => void + onCollapseAll?: () => void + onSubChatSelect: (workspaceId: string, subChat: SubChatMeta, isRemote: boolean) => void + onCreateSubChat: (workspaceId: string) => void + searchQuery?: string + // Sort controls + sortMode: "recent" | "alpha" + onToggleSort: () => void + // Accent color + onNavigateToSettings: (projectId: string) => void } // Memoized Chat List Section component @@ -932,9 +1373,29 @@ const ChatListSection = React.memo(function ChatListSection({ nameRefCallback, formatTime, justCreatedIds, + expandedSet, + onToggleExpand, + onCollapseAll, + onSubChatSelect, + onCreateSubChat, + searchQuery, + sortMode, + onToggleSort, + onNavigateToSettings, + showHeader = true, }: ChatListSectionProps) { if (chats.length === 0) return null + // Auto-expand all when searching, otherwise respect user toggle + const effectiveExpandedSet = useMemo(() => { + if (searchQuery?.trim()) { + const allIds = new Set(expandedSet) + chats.forEach((c) => allIds.add(c.id)) + return allIds + } + return expandedSet + }, [expandedSet, searchQuery, chats]) + // Pre-compute global indices map to avoid O(n²) findIndex in map() const globalIndexMap = useMemo(() => { const map = new Map() @@ -944,17 +1405,74 @@ const ChatListSection = React.memo(function ChatListSection({ return ( <> -
-

- {title} -

-
-
+ {showHeader && ( +
+

+ Threads +

+ {/* Codex-style action icons — collapse all, sort, new workspace */} + {!isMultiSelectMode && ( +
+ + + + + + Collapse all + + + + + + + + {sortMode === "recent" ? "Sort A-Z" : "Sort by recent"} + + + + + + + + New workspace + + +
+ )} +
+ )} +
{chats.map((chat) => { const isLoading = loadingChatIds.has(chat.id) // For remote chats, compare without prefix; for local, compare directly @@ -970,11 +1488,19 @@ const ChatListSection = React.memo(function ChatListSection({ const repoName = chat.isRemote ? chat.meta?.repository : (project?.gitRepo || project?.name) + // Build a helpful subtitle: "owner/repo · branch" or "~/Code/project" shorthand + const projectPath = project?.path + ? project.path.replace(/^\/Users\/[^/]+/, "~") // Shorten home dir to ~ + : null const displayText = chat.branch ? repoName - ? `${repoName} • ${chat.branch}` + ? `${repoName} · ${chat.branch}` : chat.branch - : repoName || (chat.isRemote ? "Remote project" : "Local project") + : repoName + ? projectPath + ? `${repoName} · ${projectPath}` + : repoName + : projectPath || (chat.isRemote ? "Remote project" : "") const isChecked = selectedChatIds.has(chat.id) // TODO: remote stats disabled — backend no longer computes them (was causing 50s+ loads) @@ -984,6 +1510,8 @@ const ChatListSection = React.memo(function ChatListSection({ const hasPendingQuestion = workspacePendingQuestions.has(chat.id) const isLastInFilteredChats = globalIndex === filteredChats.length - 1 const isJustCreated = justCreatedIds.has(chat.id) + // Derive accent color from the project (set in project settings) + const accentColor = project?.accentColor ?? null // For remote chats, extract gitOwner from meta.repository (e.g. "owner/repo" -> "owner") const gitOwner = chat.isRemote @@ -992,56 +1520,98 @@ const ChatListSection = React.memo(function ChatListSection({ const gitProvider = chat.isRemote ? 'github' : project?.gitProvider return ( - +
+
{ + e.stopPropagation() + onChatClick(chat.id, e) + }} + > +
+ onToggleExpand(chat.id)} + isLoading={isLoading} + hasUnseenChanges={unseenChanges.has(chat.id)} + hasPendingPlan={hasPendingPlan} + hasPendingQuestion={hasPendingQuestion} + isMultiSelectMode={isMultiSelectMode} + isChecked={isChecked} + isFocused={isFocused} + isMobileFullscreen={isMobileFullscreen} + isDesktop={isDesktop} + isPinned={isPinned} + displayText={displayText} + gitOwner={gitOwner} + gitProvider={gitProvider} + stats={stats ?? undefined} + selectedChatIdsSize={selectedChatIds.size} + canShowPinOption={canShowPinOption} + areAllSelectedPinned={areAllSelectedPinned} + filteredChatsLength={filteredChats.length} + isLastInFilteredChats={isLastInFilteredChats} + showIcon={true} + onChatClick={onChatClick} + onCheckboxClick={onCheckboxClick} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + onArchive={onArchive} + onTogglePin={onTogglePin} + onRenameClick={onRenameClick} + onCopyBranch={onCopyBranch} + onArchiveAllBelow={onArchiveAllBelow} + onArchiveOthers={onArchiveOthers} + onOpenLocally={onOpenLocally} + onBulkPin={onBulkPin} + onBulkUnpin={onBulkUnpin} + onBulkArchive={onBulkArchive} + archivePending={archivePending} + archiveBatchPending={archiveBatchPending} + isRemote={chat.isRemote} + nameRefCallback={nameRefCallback} + formatTime={formatTime} + isJustCreated={isJustCreated} + onCreateSubChat={() => onCreateSubChat(chat.isRemote ? chat.id.replace(/^remote_/, '') : chat.id)} + accentColor={accentColor} + onNavigateToSettings={onNavigateToSettings} + /> +
+
+ {/* Sub-chats list when workspace is expanded (or when searching) */} + + {effectiveExpandedSet.has(chat.id) && ( + +
+ +
+
+ )} +
+
) })}
@@ -1054,7 +1624,7 @@ interface AgentsSidebarProps { clerkUser?: any desktopUser?: { id: string; email: string; name?: string } | null onSignOut?: () => void - onToggleSidebar?: () => void + onToggleSidebar?: (e?: React.MouseEvent) => void isMobileFullscreen?: boolean onChatSelect?: () => void } @@ -1066,10 +1636,10 @@ const ArchiveButton = memo(forwardRef - + ) } @@ -1103,9 +1673,9 @@ const KanbanButton = memo(function KanbanButton() { @@ -1182,14 +1752,14 @@ const InboxButton = memo(function InboxButton() { type="button" onClick={handleClick} className={cn( - "flex items-center gap-2.5 w-full pl-2 pr-2 py-1.5 rounded-md text-sm transition-colors duration-150", + "flex items-center gap-2.5 w-full px-2.5 py-1.5 rounded-lg text-[13px] transition-[background-color,color,border-color,opacity,transform] duration-150 ease-out border border-border/50", isActive - ? "bg-foreground/5 text-foreground" - : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground", + ? "bg-foreground/[0.08] text-foreground/90 border-border/60" + : "text-muted-foreground/80 hover:bg-foreground/[0.06] hover:text-foreground hover:border-border/70 active:scale-[0.98]", )} > - - Inbox + + Inbox {inboxUnreadCount > 0 && ( {inboxUnreadCount > 99 ? "99+" : inboxUnreadCount} @@ -1214,12 +1784,12 @@ const AutomationsButton = memo(function AutomationsButton() { type="button" onClick={handleClick} className={cn( - "group flex items-center gap-2.5 w-full pl-2 pr-2 py-1.5 rounded-md text-sm transition-colors duration-150", - "text-muted-foreground hover:bg-foreground/5 hover:text-foreground", + "group flex items-center gap-2.5 w-full px-2.5 py-1.5 rounded-lg text-[13px] transition-[background-color,color,border-color,opacity,transform] duration-150 ease-out border border-border/50", + "text-muted-foreground/80 hover:bg-foreground/[0.06] hover:text-foreground hover:border-border/70 active:scale-[0.98]", )} > - - Automations + + Automations ) @@ -1277,323 +1847,58 @@ interface SidebarHeaderProps { userId: string | null | undefined desktopUser: { id: string; email: string; name?: string } | null onSignOut: () => void - onToggleSidebar?: () => void + onToggleSidebar?: (e?: React.MouseEvent) => void setSettingsDialogOpen: (open: boolean) => void - setSettingsActiveTab: (tab: string) => void + setSettingsActiveTab: (tab: SettingsTab) => void setShowAuthDialog: (open: boolean) => void handleSidebarMouseEnter: () => void - handleSidebarMouseLeave: () => void - closeButtonRef: React.RefObject + handleSidebarMouseLeave: (e: React.MouseEvent) => void + closeButtonRef: React.RefObject + onSearchClick?: () => void } const SidebarHeader = memo(function SidebarHeader({ isDesktop, isFullscreen, isMobileFullscreen, - userId, - desktopUser, - onSignOut, - onToggleSidebar, - setSettingsDialogOpen, - setSettingsActiveTab, - setShowAuthDialog, handleSidebarMouseEnter, handleSidebarMouseLeave, - closeButtonRef, + onSearchClick, }: SidebarHeaderProps) { - const [isDropdownOpen, setIsDropdownOpen] = useState(false) - const showOfflineFeatures = useAtomValue(showOfflineModeFeaturesAtom) - const toggleSidebarHotkey = useResolvedHotkeyDisplay("toggle-sidebar") - return (
- {/* Draggable area for window movement - background layer (hidden in fullscreen) */} - {isDesktop && !isFullscreen && ( -
- )} - - {/* No-drag zone over native traffic lights */} - + {/* Spacer for macOS traffic lights — pushes sidebar content below the title bar */} + - {/* Close button - positioned at top right */} - {!isMobileFullscreen && ( + {/* Search icon — positioned in the title bar row, right of the layout toggle (managed by AgentsLayout) */} + {isDesktop && !isFullscreen && (
- - - + + - - Close sidebar - {toggleSidebarHotkey && {toggleSidebarHotkey}} - + Search
)} - - {/* Spacer for macOS traffic lights */} - - - {/* Team dropdown - below traffic lights */} -
-
-
- - - -
-
- -
-
-
- 1Code -
-
- {showOfflineFeatures && ( -
- -
- )} - -
-
-
- - {userId ? ( - <> - {/* Project section at the top */} -
-
-
-
-
- -
-
-
- {desktopUser?.name || "User"} -
-
- {desktopUser?.email} -
-
-
-
-
- - {/* Settings */} - { - setIsDropdownOpen(false) - setSettingsActiveTab("preferences") - setSettingsDialogOpen(true) - }} - > - - Settings - - - {/* Help Submenu */} - - - - Help - - - { - window.open( - "https://discord.gg/8ektTZGnj4", - "_blank", - ) - setIsDropdownOpen(false) - }} - className="gap-2" - > - - Discord - - {!isMobileFullscreen && ( - { - setIsDropdownOpen(false) - setSettingsActiveTab("keyboard") - setSettingsDialogOpen(true) - }} - className="gap-2" - > - - Shortcuts - - )} - - - - - - {/* Log out */} -
- onSignOut()} - > - - - - - - Log out - -
- - ) : ( - <> - {/* Login for unauthenticated users */} -
- { - setIsDropdownOpen(false) - setShowAuthDialog(true) - }} - > - - Login - -
- - - - {/* Help Submenu */} - - - - Help - - - { - window.open( - "https://discord.gg/8ektTZGnj4", - "_blank", - ) - setIsDropdownOpen(false) - }} - className="gap-2" - > - - Discord - - {!isMobileFullscreen && ( - { - setIsDropdownOpen(false) - setSettingsActiveTab("keyboard") - setSettingsDialogOpen(true) - }} - className="gap-2" - > - - Shortcuts - - )} - - - - )} - - -
-
-
) }) @@ -1637,10 +1942,10 @@ const HelpSection = memo(function HelpSection({ isMobile }: HelpSectionProps) {
@@ -1676,6 +1981,7 @@ export function AgentsSidebar({ const isSidebarHoveredRef = useRef(false) const closeButtonRef = useRef(null) const [searchQuery, setSearchQuery] = useState("") + const [sortMode, setSortMode] = useState<"recent" | "alpha">("recent") // Sort toggle: recent first or alphabetical const [focusedChatIndex, setFocusedChatIndex] = useState(-1) // -1 means no focus const hoveredChatIndexRef = useRef(-1) // Track hovered chat for X hotkey - ref to avoid re-renders @@ -1737,6 +2043,7 @@ export function AgentsSidebar({ // Pinned chats (stored in localStorage per project) const [pinnedChatIds, setPinnedChatIds] = useState>(new Set()) + const [collapsedGroups, setCollapsedGroups] = useState>(new Set()) const searchInputRef = useRef(null) // Agent name tooltip refs (for truncated names) - using DOM manipulation to avoid re-renders @@ -1769,7 +2076,7 @@ export function AgentsSidebar({ const showWorkspaceIcon = useAtomValue(showWorkspaceIconAtom) // Desktop: use selectedProject instead of teams - const [selectedProject] = useAtom(selectedProjectAtom) + const [selectedProject, setSelectedProject] = useAtom(selectedProjectAtom) // Keep chatSourceModeAtom for backwards compatibility (used in other places) const [chatSourceMode, setChatSourceMode] = useAtom(chatSourceModeAtom) @@ -1788,6 +2095,105 @@ export function AgentsSidebar({ } }, []) + // Get tRPC utils early — needed for cache invalidation in callbacks below + const utils = trpc.useUtils() + + // ── Hierarchical sidebar: expanded workspaces state ────────────────────── + const [expandedWorkspaceIds, setExpandedWorkspaceIds] = useAtom(expandedWorkspaceIdsAtom) + const expandedSet = useMemo(() => new Set(expandedWorkspaceIds), [expandedWorkspaceIds]) + + // Toggle workspace expansion (collapse if expanded, expand if collapsed) + const handleToggleExpand = useCallback((chatId: string) => { + setExpandedWorkspaceIds((prev) => { + const set = new Set(prev) + if (set.has(chatId)) { + set.delete(chatId) + } else { + set.add(chatId) + } + return Array.from(set) + }) + }, [setExpandedWorkspaceIds]) + + // Collapse all expanded workspaces + const handleCollapseAll = useCallback(() => { + setExpandedWorkspaceIds([]) + }, [setExpandedWorkspaceIds]) + + // Toggle sort mode between recent and alphabetical + const handleToggleSort = useCallback(() => { + setSortMode((prev) => (prev === "recent" ? "alpha" : "recent")) + }, []) + + // Auto-expand workspace only when a *different* chat is selected + // (not when the expanded set changes, which would fight user collapse) + const prevSelectedRef = useRef(null) + useEffect(() => { + if (selectedChatId && selectedChatId !== prevSelectedRef.current) { + prevSelectedRef.current = selectedChatId + setExpandedWorkspaceIds((prev) => { + if (prev.includes(selectedChatId)) return prev + return [...prev, selectedChatId] + }) + } + }, [selectedChatId, setExpandedWorkspaceIds]) + + // Handle sub-chat selection from the hierarchy tree + const handleSubChatSelect = useCallback((workspaceId: string, subChat: SubChatMeta, isRemote: boolean) => { + // Set the workspace as selected + const chatOriginalId = isRemote ? workspaceId.replace(/^remote_/, '') : workspaceId + setSelectedChatId(chatOriginalId) + setSelectedChatIsRemote(isRemote) + setChatSourceMode(isRemote ? "sandbox" : "local") + + // Set the sub-chat as active in the store + const store = useAgentSubChatStore.getState() + store.setChatId(chatOriginalId) + if (!store.openSubChatIds.includes(subChat.id)) { + store.addToOpenSubChats(subChat.id) + } + store.setActiveSubChat(subChat.id) + + // Claim chat in desktop (prevent other windows from opening same chat) + window.desktopApi?.claimChat(chatOriginalId) + }, [setSelectedChatId, setSelectedChatIsRemote, setChatSourceMode]) + + // Create a new sub-chat within a workspace + const handleCreateSubChat = useCallback(async (workspaceId: string) => { + try { + const newSubChat = await trpcClient.chats.createSubChat.mutate({ + chatId: workspaceId, + name: "Untitled", + mode: "agent", + }) + + // Expand the workspace if not already expanded + setExpandedWorkspaceIds((prev) => { + if (prev.includes(workspaceId)) return prev + return [...prev, workspaceId] + }) + + // Set the workspace as selected and navigate to the new sub-chat + setSelectedChatId(workspaceId) + const store = useAgentSubChatStore.getState() + store.setChatId(workspaceId) + store.addToAllSubChats({ + id: newSubChat.id, + name: "Untitled", + created_at: new Date().toISOString(), + mode: "agent", + }) + store.addToOpenSubChats(newSubChat.id) + store.setActiveSubChat(newSubChat.id) + window.desktopApi?.claimChat(workspaceId) + + // Invalidate the chat query so WorkspaceSubChats re-fetches and shows the new thread + utils.chats.get.invalidate({ id: workspaceId }) + } catch (err) { + toast.error("Failed to create chat") + } + }, [setExpandedWorkspaceIds, setSelectedChatId, utils]) + // Fetch all local chats (no project filter) const { data: localChats } = trpc.chats.list.useQuery({}) @@ -1816,6 +2222,7 @@ export function AgentsSidebar({ baseBranch: string | null prUrl: string | null prNumber: number | null + accentColor?: string | null sandboxId?: string | null meta?: { repository?: string; branch?: string | null } | null isRemote: boolean @@ -1837,6 +2244,7 @@ export function AgentsSidebar({ baseBranch: chat.baseBranch, prUrl: chat.prUrl, prNumber: chat.prNumber, + accentColor: chat.accentColor, isRemote: false, }) } @@ -1948,9 +2356,6 @@ export function AgentsSidebar({ const { data: archivedChats } = trpc.chats.listArchived.useQuery({}) const archivedChatsCount = archivedChats?.length ?? 0 - // Get utils outside of callbacks - hooks must be called at top level - const utils = trpc.useUtils() - // Unified undo stack for workspaces and sub-chats (Jotai atom) const [undoStack, setUndoStack] = useAtom(undoStackAtom) @@ -2172,6 +2577,18 @@ export function AgentsSidebar({ }, }) + // Accent color mutation — updates workspace color with optimistic cache update + // Navigate to project settings page with the given project pre-selected + const handleNavigateToSettings = useCallback((projectId: string) => { + // Find the project to populate the selectedProjectAtom so the settings tab opens with it selected + const project = projects?.find((p) => p.id === projectId) + if (project) { + setSelectedProject({ id: project.id, name: project.name, path: project.path }) + } + setSettingsActiveTab("projects") + setSettingsDialogOpen(true) + }, [projects, setSelectedProject, setSettingsActiveTab, setSettingsDialogOpen]) + const handleTogglePin = useCallback((chatId: string) => { setPinnedChatIds((prev) => { const next = new Set(prev) @@ -2281,25 +2698,106 @@ export function AgentsSidebar({ const clerkUsername = clerkUser?.username // Filter and separate pinned/unpinned agents + // During search: show ALL workspaces (they auto-expand and sub-chats are filtered within each) + // This allows finding threads even when the parent workspace name doesn't match the query const { pinnedAgents, unpinnedAgents, filteredChats } = useMemo(() => { if (!agentChats) return { pinnedAgents: [], unpinnedAgents: [], filteredChats: [] } - const filtered = searchQuery.trim() - ? agentChats.filter((chat) => - (chat.name ?? "").toLowerCase().includes(searchQuery.toLowerCase()), - ) - : agentChats + // Keep all workspaces visible during search — sub-chat filtering happens inside + // WorkspaceSubChats, and workspace names are visually dimmed when they don't match + let sorted = [...agentChats] + + // Apply sort mode: "alpha" sorts alphabetically, "recent" is already sorted by updatedAt + if (sortMode === "alpha") { + sorted.sort((a, b) => { + const aName = (a.name ?? "").toLowerCase() + const bName = (b.name ?? "").toLowerCase() + return aName.localeCompare(bName) + }) + } - const pinned = filtered.filter((chat) => pinnedChatIds.has(chat.id)) - const unpinned = filtered.filter((chat) => !pinnedChatIds.has(chat.id)) + const pinned = sorted.filter((chat) => pinnedChatIds.has(chat.id)) + const unpinned = sorted.filter((chat) => !pinnedChatIds.has(chat.id)) return { pinnedAgents: pinned, unpinnedAgents: unpinned, filteredChats: [...pinned, ...unpinned], } - }, [searchQuery, agentChats, pinnedChatIds]) + }, [searchQuery, agentChats, pinnedChatIds, sortMode]) + + // Group chats by project for the sidebar hierarchy (owner/repo grouping) + type ChatType = typeof filteredChats extends (infer T)[] ? T : never + const projectGroupedChats = useMemo(() => { + const groups: Array<{ + key: string + label: string + projectId: string | null + chats: ChatType[] + }> = [] + const groupMap = new Map() + const groupOrder: string[] = [] + const projectIdsWithChats = new Set() + + for (const chat of filteredChats) { + const project = chat.projectId ? projectsMap.get(chat.projectId) : null + if (chat.projectId) projectIdsWithChats.add(chat.projectId) + const groupKey = chat.isRemote + ? (chat.meta?.repository ?? "remote") + : (project ? `proj:${chat.projectId}` : "ungrouped") + + if (!groupMap.has(groupKey)) { + groupMap.set(groupKey, []) + groupOrder.push(groupKey) + } + groupMap.get(groupKey)!.push(chat) + } + + // Build groups from chats + for (const key of groupOrder) { + const chats = groupMap.get(key)! + const firstChat = chats[0] + const project = firstChat?.projectId ? projectsMap.get(firstChat.projectId) : null + + let label = key + if (key === "ungrouped") { + label = "Unlinked" + } else if (key === "remote") { + label = "Remote" + } else if (project) { + const owner = project.gitOwner + const repo = project.gitRepo || project.name + label = owner && repo ? `${owner}/${repo}` : repo || project.name || key + } + + groups.push({ + key, + label, + projectId: firstChat?.projectId ?? null, + chats, + }) + } + + // Add projects that have no chats (show as empty groups) + if (projects) { + for (const project of projects) { + if (!projectIdsWithChats.has(project.id)) { + const owner = project.gitOwner + const repo = project.gitRepo || project.name + const label = owner && repo ? `${owner}/${repo}` : repo || project.name || "Project" + groups.push({ + key: `proj:${project.id}`, + label, + projectId: project.id, + chats: [], + }) + } + } + } + + return groups + }, [filteredChats, projectsMap, projects]) // Handle bulk archive of selected chats const handleBulkArchive = useCallback(() => { @@ -2656,11 +3154,16 @@ export function AgentsSidebar({ setChatSourceMode(isRemote ? "sandbox" : "local") setShowNewChatForm(false) // Clear new chat form state when selecting a workspace setDesktopView(null) // Clear automations/inbox view when selecting a chat + + // Toggle expand/collapse when re-clicking an already-selected workspace + if (selectedChatId === originalId) { + handleToggleExpand(chatId) + } // On mobile, notify parent to switch to chat mode if (isMobileFullscreen && onChatSelect) { onChatSelect() } - }, [filteredChats, selectedChatId, selectedChatIds, toggleChatSelection, setSelectedChatIds, setSelectedChatId, setSelectedChatIsRemote, setChatSourceMode, setShowNewChatForm, setDesktopView, isMobileFullscreen, onChatSelect]) + }, [filteredChats, selectedChatId, selectedChatIds, toggleChatSelection, setSelectedChatIds, setSelectedChatId, setSelectedChatIsRemote, setChatSourceMode, setShowNewChatForm, setDesktopView, isMobileFullscreen, onChatSelect, handleToggleExpand]) const handleCheckboxClick = useCallback((e: React.MouseEvent, chatId: string) => { e.stopPropagation() @@ -3115,7 +3618,7 @@ export function AgentsSidebar({ data-mobile-fullscreen={isMobileFullscreen || undefined} data-sidebar-content > - {/* Header area - isolated component to prevent re-renders when dropdown opens */} + {/* Header area */} searchInputRef.current?.focus()} /> - {/* Search and New Workspace */} -
-
- {/* Search Input */} -
- setSearchQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Escape") { - e.preventDefault() - searchInputRef.current?.blur() - setFocusedChatIndex(-1) // Reset focus - return - } - - if (e.key === "ArrowDown") { - e.preventDefault() - setFocusedChatIndex((prev) => { - // If no focus yet, start from first item - if (prev === -1) return 0 - // Otherwise move down - return prev < filteredChats.length - 1 ? prev + 1 : prev - }) - return - } - - if (e.key === "ArrowUp") { - e.preventDefault() - setFocusedChatIndex((prev) => { - // If no focus yet, start from last item - if (prev === -1) return filteredChats.length - 1 - // Otherwise move up - return prev > 0 ? prev - 1 : prev - }) - return - } + {/* Hidden search input for keyboard-triggered search */} + setSearchQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Escape") { + e.preventDefault() + setSearchQuery("") + searchInputRef.current?.blur() + setFocusedChatIndex(-1) + return + } + if (e.key === "ArrowDown") { + e.preventDefault() + setFocusedChatIndex((prev) => prev === -1 ? 0 : Math.min(prev + 1, filteredChats.length - 1)) + return + } + if (e.key === "ArrowUp") { + e.preventDefault() + setFocusedChatIndex((prev) => prev === -1 ? filteredChats.length - 1 : Math.max(prev - 1, 0)) + return + } + if (e.key === "Enter") { + e.preventDefault() + if (focusedChatIndex >= 0) { + const focusedChat = filteredChats[focusedChatIndex] + if (focusedChat) { + handleChatClick(focusedChat.id) + searchInputRef.current?.blur() + setSearchQuery("") + setFocusedChatIndex(-1) + } + } + return + } + }} + onBlur={() => { + if (!searchQuery) setFocusedChatIndex(-1) + }} + className={cn( + "rounded-md text-[12.5px] bg-transparent border border-border/30 placeholder:text-muted-foreground/25 focus:bg-foreground/[0.03] focus:border-border/60 focus-visible:ring-0 focus-visible:ring-offset-0 px-2.5 transition-all duration-200 mx-3 mb-1", + searchQuery ? "h-7 opacity-100" : "h-0 opacity-0 overflow-hidden border-0 p-0 m-0", + )} + /> - if (e.key === "Enter") { - e.preventDefault() - // Only open if something is focused (not -1) - if (focusedChatIndex >= 0) { - const focusedChat = filteredChats[focusedChatIndex] - if (focusedChat) { - handleChatClick(focusedChat.id) - searchInputRef.current?.blur() - setFocusedChatIndex(-1) // Reset focus after selection - } - } - return - } - }} - className={cn( - "w-full rounded-lg text-sm bg-muted border border-input placeholder:text-muted-foreground/40", - isMobileFullscreen ? "h-10" : "h-7", - )} - /> -
- {/* New Workspace Button */} - - - - New Workspace - - - - Start a new workspace - {newWorkspaceHotkey && ( - - {newWorkspaceHotkey} - {newWorkspaceAltHotkey && <>or{newWorkspaceAltHotkey}} - - )} - - -
-
+ {/* Navigation — New Agent + Marketplace */} +
+ - {/* Navigation Links - Inbox & Automations */} -
- - +
- {/* Scrollable Agents List */} + {/* Project-grouped agents list */}
- {/* Drafts Section - always show regardless of chat source mode */} - {drafts.length > 0 && !searchQuery && ( -
-
-

- Drafts -

-
-
- {drafts.map((draft) => ( - - ))} + {projectGroupedChats.map((group) => { + const isGroupCollapsed = collapsedGroups.has(group.key) + + return ( +
+ {/* Project header */} + + +
setCollapsedGroups(prev => { + const next = new Set(prev) + if (next.has(group.key)) next.delete(group.key) + else next.add(group.key) + return next + })} + > + + {group.label} + + {group.projectId && ( + + )} +
+
+ + { + setCollapsedGroups(prev => { + const next = new Set(prev) + if (next.has(group.key)) next.delete(group.key) + else next.add(group.key) + return next + }) + }}> + {isGroupCollapsed ? "Expand" : "Collapse"} + + {group.projectId && ( + <> + handleNavigateToSettings(group.projectId!)}> + Project settings + + + { + setSelectedProject(projects?.find(p => p.id === group.projectId) as any ?? null) + handleNewAgent() + }}> + New agent + + + )} + +
+ + {/* Agents list */} + + {!isGroupCollapsed && ( + + {group.chats.length > 0 && group.chats.map((chat) => { + const chatOriginalId = chat.isRemote ? chat.id.replace(/^remote_/, '') : chat.id + const isSelected = selectedChatId === chatOriginalId && selectedChatIsRemote === chat.isRemote + const isLoading = loadingChatIds.has(chat.id) + const hasPendingQuestion = workspacePendingQuestions.has(chat.id) + const hasPendingPlan = workspacePendingPlans.has(chat.id) + const isActive = isLoading || hasPendingQuestion || hasPendingPlan + + return ( + + + + + + + handleRenameClick({ id: chat.id, name: chat.name, isRemote: chat.isRemote })}> + Rename + + handleArchiveSingle(chat.id)}> + Archive + + {chat.branch && ( + <> + + handleCopyBranch(chat.branch!)}> + Copy branch name + + + )} + + handleArchiveOthers(chat.id)}> + Archive others + + + + ) + })} + + )} +
-
- )} - - {/* Chats Section */} - {filteredChats.length > 0 ? ( -
- {/* Pinned section */} - + ) + })} - {/* Unpinned section */} - 0 ? "Recent workspaces" : "Workspaces"} - chats={unpinnedAgents} - selectedChatId={selectedChatId} - selectedChatIsRemote={selectedChatIsRemote} - focusedChatIndex={focusedChatIndex} - loadingChatIds={loadingChatIds} - unseenChanges={unseenChanges} - workspacePendingPlans={workspacePendingPlans} - workspacePendingQuestions={workspacePendingQuestions} - isMultiSelectMode={isMultiSelectMode} - selectedChatIds={selectedChatIds} - isMobileFullscreen={isMobileFullscreen} - isDesktop={isDesktop} - pinnedChatIds={pinnedChatIds} - projectsMap={projectsMap} - workspaceFileStats={workspaceFileStats} - filteredChats={filteredChats} - canShowPinOption={canShowPinOption} - areAllSelectedPinned={areAllSelectedPinned} - showIcon={showWorkspaceIcon} - onChatClick={handleChatClick} - onCheckboxClick={handleCheckboxClick} - onMouseEnter={handleAgentMouseEnter} - onMouseLeave={handleAgentMouseLeave} - onArchive={handleArchiveSingle} - onTogglePin={handleTogglePin} - onRenameClick={handleRenameClick} - onCopyBranch={handleCopyBranch} - onArchiveAllBelow={handleArchiveAllBelow} - onArchiveOthers={handleArchiveOthers} - onOpenLocally={handleOpenLocally} - onBulkPin={handleBulkPin} - onBulkUnpin={handleBulkUnpin} - onBulkArchive={handleBulkArchive} - archivePending={archiveChatMutation.isPending || archiveRemoteChatMutation.isPending} - archiveBatchPending={archiveChatsBatchMutation.isPending || archiveRemoteChatsBatchMutation.isPending} - nameRefCallback={nameRefCallback} - formatTime={formatTime} - justCreatedIds={justCreatedIds} - /> + {/* Empty state when no projects have chats */} + {projectGroupedChats.length === 0 && ( +
+ No workspaces yet
- ) : null} + )}
- {/* Top gradient fade (appears when scrolled down) */} - {/* Top gradient fade (appears when scrolled down) */} + {/* Top gradient */}
- - {/* Bottom gradient fade */} + {/* Bottom gradient */}
- {/* Footer - Multi-select toolbar or normal footer */} - - {isMultiSelectMode ? ( - { - hasFooterAnimated.current = true - }} - className="p-2 flex flex-col gap-2" - > - {/* Selection info */} -
- - {selectedChatsCount} selected - - -
- - {/* Action buttons */} -
- -
-
- ) : ( - { - hasFooterAnimated.current = true - }} - className="p-2 pt-2 flex flex-col gap-2" - > -
-
- {/* Settings Button */} - - - - - Settings{settingsHotkey && <> {settingsHotkey}} - - - {/* Help Button - isolated component to prevent sidebar re-renders */} - - - {/* Kanban View Button - isolated component */} - - - {/* Archive Button - isolated component to prevent sidebar re-renders */} - -
- -
-
- - {/* Feedback Button */} - window.open(FEEDBACK_URL, "_blank")} - variant="outline" - size="sm" - className={cn( - "px-2 w-full hover:bg-foreground/10 transition-[background-color,transform] duration-150 ease-out active:scale-[0.97] text-foreground rounded-lg gap-1.5", - isMobileFullscreen ? "h-10" : "h-7", - )} - > - Feedback - - - )} - + {/* Footer — Open Workspace */} +
+ +
) diff --git a/src/renderer/features/terminal/atoms.ts b/src/renderer/features/terminal/atoms.ts index d6fc7ea39..a0b38a10f 100644 --- a/src/renderer/features/terminal/atoms.ts +++ b/src/renderer/features/terminal/atoms.ts @@ -59,6 +59,15 @@ export const terminalBottomHeightAtom = atomWithStorage( // Terminal search open state - maps paneId to search visibility export const terminalSearchOpenAtom = atom>({}) +// Terminal font size preference (persisted to localStorage) +export type TerminalFontSize = 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 20 | 24 +export const terminalFontSizeAtom = atomWithStorage( + "preferences:terminal-font-size", + 13, // Default matches the previous hardcoded value + undefined, + { getOnInit: true }, +) + // ============================================================================ // Multi-Terminal State Management // ============================================================================ diff --git a/src/renderer/features/terminal/config.ts b/src/renderer/features/terminal/config.ts index 8d50d0c75..130227c44 100644 --- a/src/renderer/features/terminal/config.ts +++ b/src/renderer/features/terminal/config.ts @@ -132,11 +132,23 @@ export function getTerminalThemeFromVSCode( return extractTerminalTheme(themeColors) } +/** + * Returns appropriate line height for a given font size. + * Smaller fonts need tighter line height to avoid feeling overly spaced out; + * larger fonts can breathe more. Values tuned for terminal readability. + */ +export function getTerminalLineHeight(fontSize: number): number { + if (fontSize <= 11) return 1.3 + if (fontSize <= 13) return 1.35 + if (fontSize <= 16) return 1.4 + return 1.45 +} + export const TERMINAL_OPTIONS: ITerminalOptions = { cursorBlink: true, // Font size matches app's compact UI (text-xs = 12px, text-sm = 14px) fontSize: 13, - lineHeight: 1.4, + lineHeight: getTerminalLineHeight(13), fontFamily: TERMINAL_FONT_FAMILY, theme: TERMINAL_THEME_DARK, // Default, will be overridden dynamically allowProposedApi: true, diff --git a/src/renderer/features/terminal/helpers.ts b/src/renderer/features/terminal/helpers.ts index 4380ae0e1..d32781a25 100644 --- a/src/renderer/features/terminal/helpers.ts +++ b/src/renderer/features/terminal/helpers.ts @@ -5,7 +5,7 @@ import { CanvasAddon } from "@xterm/addon-canvas" import { SerializeAddon } from "@xterm/addon-serialize" import { WebLinksAddon } from "@xterm/addon-web-links" import type { ITheme } from "xterm" -import { TERMINAL_OPTIONS, TERMINAL_THEME_DARK, TERMINAL_THEME_LIGHT, getTerminalTheme, RESIZE_DEBOUNCE_MS } from "./config" +import { TERMINAL_OPTIONS, TERMINAL_THEME_DARK, TERMINAL_THEME_LIGHT, getTerminalTheme, getTerminalLineHeight, RESIZE_DEBOUNCE_MS } from "./config" import { FilePathLinkProvider } from "./link-providers" import { isMac, isModifierPressed, showLinkPopup, removeLinkPopup } from "./link-providers/link-popup" import { suppressQueryResponses } from "./suppressQueryResponses" @@ -69,6 +69,7 @@ export interface CreateTerminalOptions { cwd?: string initialTheme?: ITheme | null isDark?: boolean + fontSize?: number onFileLinkClick?: (path: string, line?: number, column?: number) => void onUrlClick?: (url: string) => void } @@ -89,7 +90,7 @@ export function createTerminalInstance( container: HTMLDivElement, options: CreateTerminalOptions = {} ): TerminalInstance { - const { initialTheme, isDark = true, onFileLinkClick, onUrlClick } = options + const { initialTheme, isDark = true, fontSize, onFileLinkClick, onUrlClick } = options // Debug: Check container dimensions const rect = container.getBoundingClientRect() @@ -101,7 +102,13 @@ export function createTerminalInstance( // Use provided theme, or get theme based on isDark const theme = initialTheme ?? getTerminalTheme(isDark) - const terminalOptions = { ...TERMINAL_OPTIONS, theme } + // Merge options — fontSize override takes precedence over the default in TERMINAL_OPTIONS + // Line height scales with font size so smaller sizes don't feel too spaced out + const terminalOptions = { + ...TERMINAL_OPTIONS, + theme, + ...(fontSize != null && { fontSize, lineHeight: getTerminalLineHeight(fontSize) }), + } // 1. Create xterm instance console.log("[Terminal:create] Step 1: Creating XTerm instance") diff --git a/src/renderer/features/terminal/terminal.tsx b/src/renderer/features/terminal/terminal.tsx index 630778018..387dcffc2 100644 --- a/src/renderer/features/terminal/terminal.tsx +++ b/src/renderer/features/terminal/terminal.tsx @@ -7,8 +7,9 @@ import { useTheme } from "next-themes" import { useSetAtom, useAtomValue } from "jotai" import { toast } from "sonner" import { trpc } from "@/lib/trpc" -import { terminalCwdAtom } from "./atoms" +import { terminalCwdAtom, terminalFontSizeAtom } from "./atoms" import { fullThemeDataAtom } from "@/lib/atoms" +import { getTerminalLineHeight } from "./config" import { createTerminalInstance, getDefaultTerminalBg, @@ -57,6 +58,9 @@ export function Terminal({ // VS Code theme data (if a full theme is selected) const fullThemeData = useAtomValue(fullThemeDataAtom) + // Terminal font size preference + const terminalFontSize = useAtomValue(terminalFontSizeAtom) + // Ref for terminalCwd to avoid effect re-runs when cwd changes const terminalCwdRef = useRef(terminalCwd) terminalCwdRef.current = terminalCwd @@ -154,6 +158,7 @@ export function Terminal({ { cwd: terminalCwdRef.current || cwd, isDark, + fontSize: terminalFontSize, onFileLinkClick: (path, line, column) => { console.log("[Terminal] File link clicked:", path, line, column) // TODO: Open file in editor @@ -365,6 +370,20 @@ export function Terminal({ } }, [isDark, fullThemeData]) + // Update font size + line height live when the preference changes (without recreating terminal) + useEffect(() => { + if (xtermRef.current && fitAddonRef.current) { + xtermRef.current.options.fontSize = terminalFontSize + xtermRef.current.options.lineHeight = getTerminalLineHeight(terminalFontSize) + // Refit so column/row counts adjust to the new sizing + try { + fitAddonRef.current.fit() + } catch { + // FitAddon can throw if container has zero dimensions + } + } + }, [terminalFontSize]) + // Keyboard shortcut for search useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -393,8 +412,7 @@ export function Terminal({ // Get file paths (Electron exposes webUtils) const paths = files.map((file) => { - // @ts-expect-error - Electron's webUtils API - return window.webUtils?.getPathForFile?.(file) || file.name + return (window as any).webUtils?.getPathForFile?.(file) || file.name }) const text = shellEscapePaths(paths) diff --git a/src/renderer/index.html b/src/renderer/index.html index c816b405c..5d9df09bb 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -78,6 +78,12 @@ document.getElementById("root").innerHTML = "
Error: " + msg + "
"; }; + + diff --git a/src/renderer/lib/mock-api.ts b/src/renderer/lib/mock-api.ts index c96bf1023..d3336645e 100644 --- a/src/renderer/lib/mock-api.ts +++ b/src/renderer/lib/mock-api.ts @@ -455,8 +455,8 @@ export const api = { }, // Stubs for features not needed in desktop teams: { - getUserTeams: { useQuery: () => ({ data: [], isLoading: false }) }, - getTeam: { useQuery: () => ({ data: null, isLoading: false }) }, + getUserTeams: { useQuery: (_args?: any, _opts?: any) => ({ data: [], isLoading: false }) }, + getTeam: { useQuery: (_args?: any, _opts?: any) => ({ data: null, isLoading: false }) }, updateTeam: { useMutation: () => ({ mutate: () => {}, diff --git a/src/renderer/lib/remote-api.ts b/src/renderer/lib/remote-api.ts index fac572702..57114edc3 100644 --- a/src/renderer/lib/remote-api.ts +++ b/src/renderer/lib/remote-api.ts @@ -58,7 +58,7 @@ export const remoteApi = { */ async getTeams(): Promise { const teams = await remoteTrpc.teams.getUserTeams.query() - return teams.map((t) => ({ id: t.id, name: t.name })) + return teams.map((t: any) => ({ id: t.id, name: t.name })) }, /** diff --git a/src/renderer/lib/remote-trpc.ts b/src/renderer/lib/remote-trpc.ts index fc76749d2..090f1ca09 100644 --- a/src/renderer/lib/remote-trpc.ts +++ b/src/renderer/lib/remote-trpc.ts @@ -3,7 +3,9 @@ * Uses signedFetch via IPC for authentication (no CORS issues) */ import { createTRPCClient, httpLink } from "@trpc/client" -import type { AppRouter } from "../../../../web/server/api/root" +// TODO: Import proper AppRouter type when web package is available locally +// The web backend types aren't available in this repo, so we use `any` as a fallback +type AppRouter = any import SuperJSON from "superjson" // Placeholder URL - actual base is fetched dynamically from main process @@ -56,7 +58,9 @@ const signedFetch: typeof fetch = async (input, init) => { * tRPC client connected to web backend * Fully typed, handles superjson automatically */ -export const remoteTrpc = createTRPCClient({ +// Cast to `any` because the web backend AppRouter type isn't available in the desktop repo. +// All remote API calls go through remote-api.ts which handles the typing. +export const remoteTrpc: any = createTRPCClient({ links: [ httpLink({ url: TRPC_PLACEHOLDER, diff --git a/tsconfig.json b/tsconfig.json index c94a8d76a..a6878aa25 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,8 +12,6 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "declaration": true, - "declarationMap": true, "outDir": "./dist", "rootDir": "./src", "paths": {