diff --git a/bun.lock b/bun.lock index 1e21c05cc3..8dae875180 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@dnd-kit/abstract": "0.5.0", "@dnd-kit/dom": "0.5.0", @@ -93,7 +93,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.17.13", + "version": "1.17.14", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -141,7 +141,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "0.0.1", + "version": "1.17.14", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -155,7 +155,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -191,7 +191,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -218,7 +218,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -240,7 +240,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -264,7 +264,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -284,7 +284,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.17.13", + "version": "1.17.14", "bin": { "opencode": "./bin/opencode", }, @@ -378,7 +378,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -432,7 +432,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -446,7 +446,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "effect": "catalog:", }, @@ -458,7 +458,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -490,7 +490,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -506,7 +506,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -537,7 +537,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -556,7 +556,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.17.13", + "version": "1.17.14", "bin": { "codev-code": "./bin/opencode", }, @@ -687,7 +687,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -704,9 +704,9 @@ "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.3.4", - "@opentui/keymap": ">=0.3.4", - "@opentui/solid": ">=0.3.4", + "@opentui/core": ">=0.4.3", + "@opentui/keymap": ">=0.4.3", + "@opentui/solid": ">=0.4.3", }, "optionalPeers": [ "@opentui/core", @@ -763,7 +763,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "cross-spawn": "catalog:", }, @@ -778,7 +778,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -793,7 +793,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -837,7 +837,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -850,7 +850,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@ibm/plex": "6.4.1", "@opencode-ai/stats-core": "workspace:*", @@ -883,7 +883,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -902,7 +902,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -943,7 +943,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -970,7 +970,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1021,7 +1021,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.17.13", + "version": "1.17.14", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -1096,9 +1096,9 @@ "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@opentui/core": "0.3.4", - "@opentui/keymap": "0.3.4", - "@opentui/solid": "0.3.4", + "@opentui/core": "0.4.3", + "@opentui/keymap": "0.4.3", + "@opentui/solid": "0.4.3", "@pierre/diffs": "1.2.10", "@playwright/test": "1.59.1", "@sentry/solid": "10.36.0", @@ -2031,27 +2031,27 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], - "@opentui/core": ["@opentui/core@0.3.4", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.3.4", "@opentui/core-darwin-x64": "0.3.4", "@opentui/core-linux-arm64": "0.3.4", "@opentui/core-linux-arm64-musl": "0.3.4", "@opentui/core-linux-x64": "0.3.4", "@opentui/core-linux-x64-musl": "0.3.4", "@opentui/core-win32-arm64": "0.3.4", "@opentui/core-win32-x64": "0.3.4" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-y0DlrChP9lcJ4jC5z/1wMS34+ygfSTW7gD5OJHwJaAScfmlFvuJOZbwmCGrJURZ+5wFBxuOi9LatZsmeAUIKAA=="], + "@opentui/core": ["@opentui/core@0.4.3", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.3", "@opentui/core-darwin-x64": "0.4.3", "@opentui/core-linux-arm64": "0.4.3", "@opentui/core-linux-arm64-musl": "0.4.3", "@opentui/core-linux-x64": "0.4.3", "@opentui/core-linux-x64-musl": "0.4.3", "@opentui/core-win32-arm64": "0.4.3", "@opentui/core-win32-x64": "0.4.3" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-rrJfAk13tALDqldYjhc78eWQ+aKq1iknJgffIOg3OwyZoqQo+p6gtuqyhmWvXIfQzlNUbpgpCPcxbXlhMnlaHQ=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4A7JYXUsZqhu9PPCe07E30ourSJYkitkwMujUyNKjM5e/dHNDVnz+5r5cO3M5snofLafc1DN7+9jEPn4UQzchQ=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p5+7AAxpxGuDGagyQfewKtmTFnN7THvTVY4FyKqUtJomNaHdQXPHztapNNzMx0DGWbwOUbVKzpL+yc3CZY3chQ=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jvm9E8n2sPhKEyKSXn9GlmJcj8WoJXJTooXb3djwjVaiimjihIj0XxHzCWhdqbDtQp+VxDFyCKoQagOOz20qhA=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+fh0vEUE0lwVC7RW5ijYLRlTLp5NfvCRj8SzxDVd7IL2j2ssB6YXcfIbXq2EW7UGnrejwPRXf1tgUrIXW9KmOw=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-0uPuHCeZxm/O7+L+iNQl8zRAfehiwYstKkT9J0uTZO64/byBCLvy5lvn1DiE/72s/nTJ5nwpLN+pQs2/WYVKLQ=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gl6qA5QJy6u8Cbt7gOtHbhhfMZ4qQDb0kEwFXHcMGmbnKzz4OHoq74D6tNjyvSQB9saoC7C6C0tvn2DcJOuNog=="], - "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-sJYUzYcSOb5PCXRlhwsse/fdsMiVomNvIwq/2TDhAANef+YPO3Br+OH9kQRbuj0bjVDmUS36SGYWSTFu2lUO+A=="], + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-8p8g8/AEq/xFGpQ7XcIFKcAqjc0QwsZcv+Ll9RbCDpUA56FGH6jfLDir0KYTNTgYXJTIrBIENI9K46VuxMUMQA=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-btYIQeNdPbN4JCrCjVB/RwMGrnRY7qWB2piNEfALSByuULKNjPKQ33PYIj38Yd01zCvCV7FotIeXEGSHx3tgCA=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-dXpJitiZdYE3hq2Pvx6e9I0uPQSOcnaLLp1pDgWAHv+3kvKSHEX//9Yr/pV/Ua6qqT7p+2D/K4vXNap/NKVo2w=="], - "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-fhmUey4oJJ2+N62xlIgAPxAl36Fa7wYffqDOT4QLpm0jfyD5xzo+wL/hr2zUqaEI439R8Iq6jHNxf/Nsx1WuuQ=="], + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/QiFpCrpU2O7vy8QYmLIQYbvAtKDgmqcVjR7dGtqSzkiQk3ktNJoo5RozG7ueXnjung1Wp0nKldKxo2Csg/OrA=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.3.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-sh432vPU+eLp8eA4I0KWKKn7D0VHbk01YTg6mA9/ihCNYHntc6LZ8/sLvsPv8CvKscMotfIkh3M5YhdS36BuXw=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Mx2zuOjrhm/z2SDS6RExIyjP/SnN/8QhhagxURUw0jQi/NssGSeAllu1cBAFFnhobJL5QLTE4FU4CRhUK9svgg=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.3.4", "", { "os": "win32", "cpu": "x64" }, "sha512-dw8FcjUZaLAjw25P3/7BarobCh/QOHn3srYaWYQdysoqyvSlPkQumpI8kV/KgpJtdITU1GW02MQC4EeLIFFalA=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NuoqvWKGXaYnmlqvu7Gg2lLI6yVMnS9OfWBvxp+7Q+McSgHFSTQmYBXaPpvQ8HikpQXE1nCeMPtuSG4PdZHe2w=="], - "@opentui/keymap": ["@opentui/keymap@0.3.4", "", { "dependencies": { "@opentui/core": "0.3.4" }, "peerDependencies": { "@opentui/react": "0.3.4", "@opentui/solid": "0.3.4", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-8fo6BZWQgCjANfbKkzPo0ghAzS1E7TlHjDDS+SUhrX01qEUO1clFTRssKluHbXd2UJY1Ehle01TV5bFmY78f8w=="], + "@opentui/keymap": ["@opentui/keymap@0.4.3", "", { "dependencies": { "@opentui/core": "0.4.3" }, "peerDependencies": { "@opentui/react": "0.4.3", "@opentui/solid": "0.4.3", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-sinX0pyQBRrEvo89PSSUbSUDIYpL3xWo81VEfec58VFoVRB5FG48/deAtvRTQfJ8w1kgbzN8hzdOXdSm61zBmw=="], - "@opentui/solid": ["@opentui/solid@0.3.4", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.3.4", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-gin1VnsVBahX0nrU3mpgh5U1qvyJBIZu4NE5mc0YnObWOEf9HVNxKY4/BpUvQPh91kT6zeOzTBvAvYK4R7g9MQ=="], + "@opentui/solid": ["@opentui/solid@0.4.3", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.3", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-RcV0+S8HMdXOASyr7HmJUBuTUIaFPzAxMDa44VftS5C2JUgrmAuWo0Njv1q3TWRB1owjHnyKhEfWGKq7A82wxw=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -3243,7 +3243,7 @@ "builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="], - "bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="], + "bun-ffi-structs": ["bun-ffi-structs@0.2.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="], "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], @@ -5591,8 +5591,6 @@ "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], diff --git a/nix/hashes.json b/nix/hashes.json index a86dbdec4f..da7fa46acc 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-NRSHbXA5jYZ2VUQ+b7d/dwg5BWap54ej3Ys9MPwgLwc=", - "aarch64-linux": "sha256-ua9ZjsF0KDuLOPjEcxhD4u5cMfjPuqoAyUftrNPr2sI=", - "aarch64-darwin": "sha256-QH3as89//yrRpWK7f96rNjiwCO5JTDYhVrUHbqQMomw=", - "x86_64-darwin": "sha256-30cIeLqasI+FUBYBttb3+MezWoQmb+UpkFWhPy4B3Xk=" + "x86_64-linux": "sha256-KTyd2ISQ4n1E3tm1LMEHtz+rKRZga+SONn5J6H0pheQ=", + "aarch64-linux": "sha256-ZIeaaqRr4JorEmmwFYuitxfYEAtrP8C6IlD+pmFRko0=", + "aarch64-darwin": "sha256-Po8FISoBzUMwJSn6nw243/hLT/gAQCrm5HbwXe2uA0g=", + "x86_64-darwin": "sha256-5TZzrCnvg1z00YP6O9U0SXjL04r+VmF7LVrqQ9BbSn4=" } } diff --git a/package.json b/package.json index a6cd9fc00e..2594d0baeb 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,9 @@ "@octokit/rest": "22.0.0", "@hono/standard-validator": "0.2.0", "@hono/zod-validator": "0.4.2", - "@opentui/core": "0.3.4", - "@opentui/keymap": "0.3.4", - "@opentui/solid": "0.3.4", + "@opentui/core": "0.4.3", + "@opentui/keymap": "0.4.3", + "@opentui/solid": "0.4.3", "@tanstack/solid-virtual": "3.13.28", "@shikijs/stream": "4.2.0", "ulid": "3.0.1", diff --git a/packages/app/package.json b/packages/app/package.json index 766f00aeb9..739dfc5ba0 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.17.13", + "version": "1.17.14", "description": "", "type": "module", "exports": { diff --git a/packages/app/src/components/command-palette.ts b/packages/app/src/components/command-palette.ts new file mode 100644 index 0000000000..b61a61977e --- /dev/null +++ b/packages/app/src/components/command-palette.ts @@ -0,0 +1,325 @@ +import { base64Encode } from "@opencode-ai/core/util/encode" +import { getFilename } from "@opencode-ai/core/util/path" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useNavigate } from "@solidjs/router" +import { createMemo, onCleanup } from "solid-js" +import { useCommand, type CommandOption } from "@/context/command" +import { useFile } from "@/context/file" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { useServerSDK, type ServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" +import { createSessionTabs } from "@/pages/session/helpers" +import { useSessionLayout } from "@/pages/session/session-layout" +import { decode64 } from "@/utils/base64" + +export type CommandPaletteEntry = { + id: string + type: "command" | "file" | "session" + title: string + description?: string + keybind?: string + category: string + option?: CommandOption + path?: string + directory?: string + sessionID?: string + archived?: number + updated?: number +} + +const ENTRY_LIMIT = 5 +const COMMON_COMMAND_IDS = [ + "session.new", + "workspace.new", + "session.previous", + "session.next", + "terminal.toggle", + "review.toggle", +] as const + +export function uniqueCommandPaletteEntries(items: CommandPaletteEntry[]) { + const seen = new Set() + return items.filter((item) => { + if (seen.has(item.id)) return false + seen.add(item.id) + return true + }) +} + +export function createCommandPaletteFileEntry(path: string, category: string): CommandPaletteEntry { + return { + id: "file:" + path, + type: "file", + title: path, + category, + path, + } +} + +export function createCommandPaletteFileOpener(onOpenFile?: (path: string) => void) { + const file = useFile() + const layout = useLayout() + const { tabs, view } = useSessionLayout() + + return (path: string) => { + const value = file.tab(path) + void tabs().open(value) + void file.load(path) + if (!view().reviewPanel.opened()) view().reviewPanel.open() + layout.fileTree.setTab("all") + onOpenFile?.(path) + tabs().setActive(value) + } +} + +export function createCommandPaletteModel(props: { filesOnly?: () => boolean; onOpenFile?: (path: string) => void }) { + const command = useCommand() + const language = useLanguage() + const layout = useLayout() + const file = useFile() + const dialog = useDialog() + const navigate = useNavigate() + const serverSDK = useServerSDK()() + const serverSync = useServerSync() + const { params, tabs } = useSessionLayout() + const openFile = createCommandPaletteFileOpener(props.onOpenFile) + const state = { cleanup: undefined as (() => void) | void, committed: false } + const filesOnly = () => props.filesOnly?.() ?? false + + const allowedCommands = createMemo(() => { + if (filesOnly()) return [] + return command.options.filter( + (option) => + !option.disabled && !option.hidden && !option.id.startsWith("suggested.") && option.id !== "file.open", + ) + }) + const commandEntries = createMemo(() => { + const category = language.t("palette.group.commands") + return allowedCommands().map((option) => createCommandEntry(option, category)) + }) + const preferredCommandEntries = createMemo(() => { + const all = allowedCommands() + const order = new Map(COMMON_COMMAND_IDS.map((id, index) => [id, index])) + const picked = all.filter((option) => order.has(option.id)) + const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT) + const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base + const category = language.t("palette.group.commands") + return sorted.map((option) => createCommandEntry(option, category)) + }) + + const tabState = createSessionTabs({ + tabs, + pathFromTab: file.pathFromTab, + normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab), + }) + const recentFileEntries = createMemo(() => { + const all = tabState.openedTabs() + const active = tabState.activeFileTab() + const order = active ? [active, ...all.filter((item) => item !== active)] : all + const seen = new Set() + const category = language.t("palette.group.files") + return order + .map((item) => file.pathFromTab(item)) + .filter((path): path is string => { + if (!path || seen.has(path)) return false + seen.add(path) + return true + }) + .slice(0, ENTRY_LIMIT) + .map((path) => createCommandPaletteFileEntry(path, category)) + }) + const rootFileEntries = createMemo(() => { + const category = language.t("palette.group.files") + return file.tree + .children("") + .filter((node) => node.type === "file") + .map((node) => node.path) + .sort((a, b) => a.localeCompare(b)) + .slice(0, ENTRY_LIMIT) + .map((path) => createCommandPaletteFileEntry(path, category)) + }) + + const projectDirectory = createMemo(() => decode64(params.dir) ?? "") + const project = createMemo(() => { + const directory = projectDirectory() + if (!directory) return undefined + return layout.projects.list().find((item) => item.worktree === directory || item.sandboxes?.includes(directory)) + }) + const workspaces = createMemo(() => { + const directory = projectDirectory() + const current = project() + if (!current) return directory ? [directory] : [] + const dirs = [current.worktree, ...(current.sandboxes ?? [])] + if (directory && !dirs.includes(directory)) return [...dirs, directory] + return dirs + }) + const homedir = createMemo(() => serverSync().data.path.home) + const sessions = createSessionEntries({ + workspaces, + label: (directory) => { + const current = project() + const kind = + current && directory === current.worktree + ? language.t("workspace.type.local") + : language.t("workspace.type.sandbox") + const [store] = serverSync().child(directory, { bootstrap: false }) + const home = homedir() + const path = home ? directory.replace(home, "~") : directory + const name = store.vcs?.branch ?? getFilename(directory) + return `${kind} : ${name || path}` + }, + load: (directory) => serverSDK.client.session.list({ directory, roots: true }), + untitled: () => language.t("command.session.new"), + category: () => language.t("command.category.session"), + }) + + const highlight = (item: CommandPaletteEntry | undefined) => { + state.cleanup?.() + state.cleanup = undefined + if (item?.type !== "command") return + state.cleanup = item.option?.onHighlight?.() + } + + const select = (item: CommandPaletteEntry | undefined) => { + if (!item) return + state.committed = true + state.cleanup = undefined + dialog.close() + if (item.type === "command") { + item.option?.onSelect?.("palette") + return + } + if (item.type === "session") { + if (!item.directory || !item.sessionID) return + navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`) + return + } + if (!item.path) return + openFile(item.path) + } + + onCleanup(() => { + if (state.committed) return + state.cleanup?.() + }) + + return { + language, + file, + commandEntries, + preferredCommandEntries, + recentFileEntries, + rootFileEntries, + sessions, + highlight, + select, + close: () => dialog.close(), + } +} + +function createCommandEntry(option: CommandOption, category: string): CommandPaletteEntry { + return { + id: "command:" + option.id, + type: "command", + title: option.title, + description: option.description, + keybind: option.keybind, + category, + option, + } +} + +function createSessionEntries(props: { + workspaces: () => string[] + label: (directory: string) => string + load: (directory: string) => ReturnType + untitled: () => string + category: () => string +}) { + const state: { + token: number + inflight: Promise | undefined + cached: CommandPaletteEntry[] | undefined + } = { token: 0, inflight: undefined, cached: undefined } + + return (text: string) => { + if (!text.trim()) { + state.token += 1 + state.inflight = undefined + state.cached = undefined + return [] as CommandPaletteEntry[] + } + if (state.cached) return state.cached + if (state.inflight) return state.inflight + + const current = state.token + const dirs = props.workspaces() + if (dirs.length === 0) return [] as CommandPaletteEntry[] + + state.inflight = Promise.all( + dirs.map((directory) => { + const description = props.label(directory) + return props + .load(directory) + .then((result) => + (result.data ?? []) + .filter((session) => !!session?.id) + .map((session) => ({ + id: session.id, + title: session.title ?? props.untitled(), + description, + directory, + archived: session.time?.archived, + updated: session.time?.updated, + })), + ) + .catch(() => [] as SessionEntryInput[]) + }), + ) + .then((results) => { + if (state.token !== current) return [] as CommandPaletteEntry[] + const seen = new Set() + const next = results + .flat() + .filter((item) => { + const key = `${item.directory}:${item.id}` + if (seen.has(key)) return false + seen.add(key) + return true + }) + .map((item) => createSessionEntry(item, props.category())) + state.cached = next + return next + }) + .catch(() => [] as CommandPaletteEntry[]) + .finally(() => { + state.inflight = undefined + }) + + return state.inflight + } +} + +type SessionEntryInput = { + directory: string + id: string + title: string + description: string + archived?: number + updated?: number +} + +function createSessionEntry(input: SessionEntryInput, category: string): CommandPaletteEntry { + return { + id: `session:${input.directory}:${input.id}`, + type: "session", + title: input.title, + description: input.description, + category, + directory: input.directory, + sessionID: input.id, + archived: input.archived, + updated: input.updated, + } +} diff --git a/packages/app/src/components/dialog-command-palette-v2.css b/packages/app/src/components/dialog-command-palette-v2.css new file mode 100644 index 0000000000..a4019928aa --- /dev/null +++ b/packages/app/src/components/dialog-command-palette-v2.css @@ -0,0 +1,220 @@ +.command-palette-v2 { + overflow: hidden; +} + +/* Anchor to the top edge of where a centered 480px-tall dialog would sit, so the + top stays put while the content-driven height grows and shrinks. */ +[data-component="dialog-v2"]:has(.command-palette-v2) { + align-items: flex-start; +} + +[data-component="dialog-v2"]:has(.command-palette-v2) [data-slot="dialog-container"] { + width: min(calc(100vw - 24px), 640px); + height: auto; + min-height: 280px; + max-height: min(calc(100vh - 96px), 480px); + margin-top: max(48px, calc((100vh - 480px) / 2)); + border-radius: 12px; + background: var(--v2-background-bg-base); + box-shadow: var(--v2-elevation-floating); +} + +.command-palette-v2-body { + display: flex; + min-height: 0; + flex: 1; + flex-direction: column; + gap: 0; + padding: 0; +} + +.command-palette-v2-search { + flex-shrink: 0; + padding: 6px; +} + +.command-palette-v2-search [data-component="text-input-v2"] { + width: 100%; + height: 36px; + border-radius: 6px; + outline: 0; + background: color-mix(in srgb, var(--v2-background-bg-layer-02) 60%, transparent); + box-shadow: none; + transition: + background-color 120ms ease-in-out, + box-shadow 120ms ease-in-out; +} + +.command-palette-v2-search [data-component="text-input-v2"]:where(:hover):not([data-disabled], [data-invalid]), +.command-palette-v2-search [data-component="text-input-v2"]:where(:focus-within):not([data-disabled], [data-invalid]) { + background: var(--v2-background-bg-layer-02); + box-shadow: none; +} + +.command-palette-v2-search [data-component="text-input-v2"] [data-slot="text-input-v2-value"] { + border-radius: 6px; + background: transparent; + gap: 8px; +} + +.command-palette-v2-search [data-component="text-input-v2"] [data-slot="text-input-v2-leading-icon"] { + padding-left: 12px; +} + +.command-palette-v2-search [data-component="text-input-v2"] [data-slot="text-input-v2-input"] { + border-radius: 6px; + background: transparent; +} + +.command-palette-v2-scroll { + min-height: 0; + flex: 1; +} + +.command-palette-v2-results { + display: flex; + flex-direction: column; + gap: 16px; + padding: 6px 6px 8px; +} + +.command-palette-v2-group { + display: flex; + flex-direction: column; + gap: 1px; +} + +.command-palette-v2-group-title { + margin: 6px 0; + padding: 0 12px; + color: var(--v2-text-text-muted); + font-size: 13px; + font-weight: 440; + line-height: 16px; + letter-spacing: -0.04px; + user-select: none; +} + +.command-palette-v2-row { + display: flex; + width: 100%; + height: 36px; + flex-shrink: 0; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 0 12px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--v2-text-text-base); + text-align: left; + cursor: default; + scroll-margin: 6px 0; +} + +.command-palette-v2-row[data-active] { + background: var(--v2-overlay-simple-overlay-hover); +} + +.command-palette-v2-row:focus-visible { + background: var(--v2-overlay-simple-overlay-hover); + outline: none; +} + +.command-palette-v2-row-main { + display: flex; + min-width: 0; + flex: 1; + align-items: center; + gap: 8px; +} + +.command-palette-v2-row-icon { + flex-shrink: 0; + color: var(--v2-icon-icon-muted); +} + +.command-palette-v2-row-text { + display: flex; + min-width: 0; + align-items: center; + gap: 6px; +} + +.command-palette-v2-title { + min-width: 0; + overflow: hidden; + color: var(--v2-text-text-base); + font-size: 13px; + font-weight: 530; + line-height: 16px; + letter-spacing: -0.04px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.command-palette-v2-description, +.command-palette-v2-meta { + min-width: 0; + overflow: hidden; + color: var(--v2-text-text-muted); + font-size: 13px; + font-weight: 440; + line-height: 16px; + letter-spacing: -0.04px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.command-palette-v2-meta { + flex-shrink: 0; +} + +.command-palette-v2-file-path { + display: flex; + min-width: 0; + align-items: baseline; + font-size: 13px; + line-height: 16px; + letter-spacing: -0.04px; +} + +.command-palette-v2-file-dir { + min-width: 0; + overflow: hidden; + color: var(--v2-text-text-muted); + font-weight: 440; + text-overflow: ellipsis; + white-space: nowrap; +} + +.command-palette-v2-file-name { + flex-shrink: 0; + color: var(--v2-text-text-base); + font-weight: 530; + white-space: nowrap; +} + +.command-palette-v2-state { + display: grid; + min-height: 120px; + place-items: center; + color: var(--v2-text-text-muted); + font-size: 13px; + font-weight: 440; + line-height: 16px; + letter-spacing: -0.04px; +} + +@media (max-width: 640px) { + .command-palette-v2-row-text { + flex-direction: column; + align-items: flex-start; + gap: 1px; + } + + .command-palette-v2-description { + max-width: 100%; + } +} diff --git a/packages/app/src/components/dialog-command-palette-v2.tsx b/packages/app/src/components/dialog-command-palette-v2.tsx new file mode 100644 index 0000000000..b1cafe1e7a --- /dev/null +++ b/packages/app/src/components/dialog-command-palette-v2.tsx @@ -0,0 +1,221 @@ +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" +import { FileIcon } from "@opencode-ai/ui/file-icon" +import { ScrollView } from "@opencode-ai/ui/scroll-view" +import { Dialog, DialogBody } from "@opencode-ai/ui/v2/dialog-v2" +import { Icon } from "@opencode-ai/ui/v2/icon" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" +import { createEffect, createMemo, createResource, createSignal, For, Match, Show, Switch } from "solid-js" +import { formatKeybindParts } from "@/context/command" +import { useLanguage } from "@/context/language" +import { getRelativeTime } from "@/utils/time" +import { + createCommandPaletteFileEntry, + createCommandPaletteModel, + uniqueCommandPaletteEntries, + type CommandPaletteEntry, +} from "./command-palette" +import "./dialog-command-palette-v2.css" + +function groups(entries: CommandPaletteEntry[]) { + const map = new Map() + for (const entry of entries) map.set(entry.category, [...(map.get(entry.category) ?? []), entry]) + return Array.from(map.entries()).map(([category, entries]) => ({ category, entries })) +} + +function matchesEntry(entry: CommandPaletteEntry, query: string) { + const value = query.toLowerCase() + return [entry.title, entry.description, entry.category].some((text) => text?.toLowerCase().includes(value)) +} + +export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => void }) { + const palette = createCommandPaletteModel(props) + const [query, setQuery] = createSignal("") + const [active, setActive] = createSignal(0) + + const loadItems = async (text: string) => { + const q = text.trim() + if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()] + + const [files, nextSessions] = await Promise.all([palette.file.searchFiles(q), Promise.resolve(palette.sessions(q))]) + const category = palette.language.t("palette.group.files") + return [ + ...palette.commandEntries().filter((entry) => matchesEntry(entry, q)), + ...nextSessions.filter((entry) => matchesEntry(entry, q)), + ...files.map((path) => createCommandPaletteFileEntry(path, category)), + ] + } + + const [entries] = createResource(query, loadItems, { initialValue: [] as CommandPaletteEntry[] }) + // Render stale results while a new query loads to avoid flashing "Loading" per keystroke. + const visibleEntries = createMemo(() => uniqueCommandPaletteEntries(entries.latest ?? [])) + const groupedEntries = createMemo(() => groups(visibleEntries())) + const activeEntry = createMemo(() => visibleEntries()[active()]) + + createEffect(() => { + query() + visibleEntries() + setActive(0) + }) + + createEffect(() => { + palette.highlight(activeEntry()) + }) + + let resultsRef: HTMLDivElement | undefined + + const move = (delta: -1 | 1) => { + const count = visibleEntries().length + if (count === 0) return + setActive((index) => (index + delta + count) % count) + requestAnimationFrame(() => { + resultsRef?.querySelector("[data-active]")?.scrollIntoView({ block: "nearest" }) + }) + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "ArrowDown") { + event.preventDefault() + move(1) + return + } + if (event.key === "ArrowUp") { + event.preventDefault() + move(-1) + return + } + if (event.key === "Enter") { + event.preventDefault() + palette.select(activeEntry()) + return + } + if (event.key === "Escape") { + event.preventDefault() + palette.close() + } + } + + return ( + + + + (resultsRef = el)}> +
+ 0} + fallback={ +
+ {entries.loading ? palette.language.t("common.loading") : palette.language.t("palette.empty")} +
+ } + > + + {(group) => ( +
+ +
{group.category}
+
+ + {(item) => ( + setActive(visibleEntries().findIndex((entry) => entry.id === item.id))} + onSelect={() => palette.select(item)} + /> + )} + +
+ )} +
+
+
+
+
+
+ ) +} + +function PaletteRow(props: { + item: CommandPaletteEntry + active: boolean + language: ReturnType + onActive: () => void + onSelect: () => void +}) { + return ( + + ) +} diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 49a0d5961a..d7c6c2a84f 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -1,329 +1,84 @@ -import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { FileIcon } from "@opencode-ai/ui/file-icon" import { Icon } from "@opencode-ai/ui/icon" import { Keybind } from "@opencode-ai/ui/keybind" import { List } from "@opencode-ai/ui/list" -import { base64Encode } from "@opencode-ai/core/util/encode" import { getDirectory, getFilename } from "@opencode-ai/core/util/path" -import { useNavigate } from "@solidjs/router" -import { createMemo, createSignal, lazy, Match, onCleanup, Show, Switch } from "solid-js" -import { formatKeybind, useCommand, type CommandOption } from "@/context/command" -import { useServerSDK, type ServerSDK } from "@/context/server-sdk" -import { useServerSync } from "@/context/server-sync" -import { useLayout } from "@/context/layout" -import { useFile } from "@/context/file" +import { createMemo, createSignal, lazy, Match, Show, Switch } from "solid-js" +import { formatKeybind } from "@/context/command" +import { useServerSDK } from "@/context/server-sdk" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { useSettings } from "@/context/settings" import { useSessionLayout } from "@/pages/session/session-layout" -import { createSessionTabs } from "@/pages/session/helpers" import { decode64 } from "@/utils/base64" import { getRelativeTime } from "@/utils/time" +import { + createCommandPaletteFileEntry, + createCommandPaletteFileOpener, + createCommandPaletteModel, + uniqueCommandPaletteEntries, + type CommandPaletteEntry, +} from "./command-palette" const DialogSelectFileV2 = lazy(() => import("./dialog-select-directory-v2").then((module) => ({ default: module.DialogSelectDirectoryV2 })), ) - -type EntryType = "command" | "file" | "session" - -type Entry = { - id: string - type: EntryType - title: string - description?: string - keybind?: string - category: string - option?: CommandOption - path?: string - directory?: string - sessionID?: string - archived?: number - updated?: number -} +const DialogCommandPaletteV2 = lazy(() => + import("./dialog-command-palette-v2").then((module) => ({ default: module.DialogCommandPaletteV2 })), +) type DialogSelectFileMode = "all" | "files" -const ENTRY_LIMIT = 5 -const COMMON_COMMAND_IDS = [ - "session.new", - "workspace.new", - "session.previous", - "session.next", - "terminal.toggle", - "review.toggle", -] as const - -const uniqueEntries = (items: Entry[]) => { - const seen = new Set() - const out: Entry[] = [] - for (const item of items) { - if (seen.has(item.id)) continue - seen.add(item.id) - out.push(item) - } - return out -} - -const createCommandEntry = (option: CommandOption, category: string): Entry => ({ - id: "command:" + option.id, - type: "command", - title: option.title, - description: option.description, - keybind: option.keybind, - category, - option, -}) - -const createFileEntry = (path: string, category: string): Entry => ({ - id: "file:" + path, - type: "file", - title: path, - category, - path, -}) - -const createSessionEntry = ( - input: { - directory: string - id: string - title: string - description: string - archived?: number - updated?: number - }, - category: string, -): Entry => ({ - id: `session:${input.directory}:${input.id}`, - type: "session", - title: input.title, - description: input.description, - category, - directory: input.directory, - sessionID: input.id, - archived: input.archived, - updated: input.updated, -}) - -function createCommandEntries(props: { - filesOnly: () => boolean - command: ReturnType - language: ReturnType -}) { - const allowed = createMemo(() => { - if (props.filesOnly()) return [] - return props.command.options.filter( - (option) => - !option.disabled && !option.hidden && !option.id.startsWith("suggested.") && option.id !== "file.open", - ) - }) - - const list = createMemo(() => { - const category = props.language.t("palette.group.commands") - return allowed().map((option) => createCommandEntry(option, category)) - }) - - const picks = createMemo(() => { - const all = allowed() - const order = new Map(COMMON_COMMAND_IDS.map((id, index) => [id, index])) - const picked = all.filter((option) => order.has(option.id)) - const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT) - const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base - const category = props.language.t("palette.group.commands") - return sorted.map((option) => createCommandEntry(option, category)) - }) - - return { allowed, list, picks } -} - -function createFileEntries(props: { - file: ReturnType - tabs: () => ReturnType["tabs"]> - language: ReturnType -}) { - const tabState = createSessionTabs({ - tabs: props.tabs, - pathFromTab: props.file.pathFromTab, - normalizeTab: (tab) => (tab.startsWith("file://") ? props.file.tab(tab) : tab), - }) - const recent = createMemo(() => { - const all = tabState.openedTabs() - const active = tabState.activeFileTab() - const order = active ? [active, ...all.filter((item) => item !== active)] : all - const seen = new Set() - const category = props.language.t("palette.group.files") - const items: Entry[] = [] - - for (const item of order) { - const path = props.file.pathFromTab(item) - if (!path) continue - if (seen.has(path)) continue - seen.add(path) - items.push(createFileEntry(path, category)) - } - - return items.slice(0, ENTRY_LIMIT) - }) - - const root = createMemo(() => { - const category = props.language.t("palette.group.files") - const nodes = props.file.tree.children("") - const paths = nodes - .filter((node) => node.type === "file") - .map((node) => node.path) - .sort((a, b) => a.localeCompare(b)) - return paths.slice(0, ENTRY_LIMIT).map((path) => createFileEntry(path, category)) - }) - - return { recent, root } -} +export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) { + const platform = usePlatform() + const settings = useSettings() + const filesOnly = () => props.mode === "files" -function createSessionEntries(props: { - workspaces: () => string[] - label: (directory: string) => string - serverSDK: ServerSDK - language: ReturnType -}) { - const state: { - token: number - inflight: Promise | undefined - cached: Entry[] | undefined - } = { - token: 0, - inflight: undefined, - cached: undefined, + if (!filesOnly() && settings.general.newLayoutDesigns()) { + return } - const sessions = (text: string) => { - const query = text.trim() - if (!query) { - state.token += 1 - state.inflight = undefined - state.cached = undefined - return [] as Entry[] - } - - if (state.cached) return state.cached - if (state.inflight) return state.inflight - - const current = state.token - const dirs = props.workspaces() - if (dirs.length === 0) return [] as Entry[] - - state.inflight = Promise.all( - dirs.map((directory) => { - const description = props.label(directory) - return props.serverSDK.client.session - .list({ directory, roots: true }) - .then((x) => - (x.data ?? []) - .filter((s) => !!s?.id) - .map((s) => ({ - id: s.id, - title: s.title ?? props.language.t("command.session.new"), - description, - directory, - archived: s.time?.archived, - updated: s.time?.updated, - })), - ) - .catch( - () => - [] as { - id: string - title: string - description: string - directory: string - archived?: number - updated?: number - }[], - ) - }), - ) - .then((results) => { - if (state.token !== current) return [] as Entry[] - const seen = new Set() - const category = props.language.t("command.category.session") - const next = results - .flat() - .filter((item) => { - const key = `${item.directory}:${item.id}` - if (seen.has(key)) return false - seen.add(key) - return true - }) - .map((item) => createSessionEntry(item, category)) - state.cached = next - return next - }) - .catch(() => [] as Entry[]) - .finally(() => { - state.inflight = undefined - }) - - return state.inflight + if (filesOnly() && platform.platform === "desktop" && settings.general.newLayoutDesigns()) { + return } - return { sessions } + return } -export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) { - const command = useCommand() +function DialogSelectFileDesktopV2(props: { onOpenFile?: (path: string) => void }) { const language = useLanguage() - const platform = usePlatform() - const settings = useSettings() - const layout = useLayout() - const file = useFile() - const dialog = useDialog() - const navigate = useNavigate() const serverSDK = useServerSDK() - const serverSync = useServerSync() - const { params, tabs, view } = useSessionLayout() - const filesOnly = () => props.mode === "files" - const state = { cleanup: undefined as (() => void) | void, committed: false } - const [grouped, setGrouped] = createSignal(false) - const commandEntries = createCommandEntries({ filesOnly, command, language }) - const fileEntries = createFileEntries({ file, tabs, language }) - + const { params } = useSessionLayout() const projectDirectory = createMemo(() => decode64(params.dir) ?? "") - const project = createMemo(() => { - const directory = projectDirectory() - if (!directory) return - return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) - }) - const workspaces = createMemo(() => { - const directory = projectDirectory() - const current = project() - if (!current) return directory ? [directory] : [] + const openFile = createCommandPaletteFileOpener(props.onOpenFile) - const dirs = [current.worktree, ...(current.sandboxes ?? [])] - if (directory && !dirs.includes(directory)) return [...dirs, directory] - return dirs - }) - const homedir = createMemo(() => serverSync().data.path.home) - const label = (directory: string) => { - const current = project() - const kind = - current && directory === current.worktree - ? language.t("workspace.type.local") - : language.t("workspace.type.sandbox") - const [store] = serverSync().child(directory, { bootstrap: false }) - const home = homedir() - const path = home ? directory.replace(home, "~") : directory - const name = store.vcs?.branch ?? getFilename(directory) - return `${kind} : ${name || path}` - } + return ( + { + if (typeof result !== "string") return + openFile(result) + }} + /> + ) +} - const { sessions } = createSessionEntries({ workspaces, label, serverSDK: serverSDK(), language }) +function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?: (path: string) => void }) { + const palette = createCommandPaletteModel(props) + const [grouped, setGrouped] = createSignal(false) const items = async (text: string) => { const query = text.trim() setGrouped(query.length > 0) - if (!query && filesOnly()) { - const loaded = file.tree.state("")?.loaded - const pending = loaded ? Promise.resolve() : file.tree.list("") - const next = uniqueEntries([...fileEntries.recent(), ...fileEntries.root()]) + if (!query && props.filesOnly()) { + const loaded = palette.file.tree.state("")?.loaded + const pending = loaded ? Promise.resolve() : palette.file.tree.list("") + const next = uniqueCommandPaletteEntries([...palette.recentFileEntries(), ...palette.rootFileEntries()]) if (loaded || next.length > 0) { void pending @@ -331,79 +86,24 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil } await pending - return uniqueEntries([...fileEntries.recent(), ...fileEntries.root()]) - } - - if (!query) return [...commandEntries.picks(), ...fileEntries.recent()] - - if (filesOnly()) { - const files = await file.searchFiles(query) - const category = language.t("palette.group.files") - return files.map((path) => createFileEntry(path, category)) + return uniqueCommandPaletteEntries([...palette.recentFileEntries(), ...palette.rootFileEntries()]) } - const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))]) - const category = language.t("palette.group.files") - const entries = files.map((path) => createFileEntry(path, category)) - return [...commandEntries.list(), ...nextSessions, ...entries] - } - - const handleMove = (item: Entry | undefined) => { - state.cleanup?.() - if (!item) return - if (item.type !== "command") return - state.cleanup = item.option?.onHighlight?.() - } - - const open = (path: string) => { - const value = file.tab(path) - void tabs().open(value) - void file.load(path) - if (!view().reviewPanel.opened()) view().reviewPanel.open() - layout.fileTree.setTab("all") - props.onOpenFile?.(path) - tabs().setActive(value) - } - - const handleSelect = (item: Entry | undefined) => { - if (!item) return - state.committed = true - state.cleanup = undefined - dialog.close() + if (!query) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()] - if (item.type === "command") { - item.option?.onSelect?.("palette") - return + if (props.filesOnly()) { + const files = await palette.file.searchFiles(query) + const category = palette.language.t("palette.group.files") + return files.map((path) => createCommandPaletteFileEntry(path, category)) } - if (item.type === "session") { - if (!item.directory || !item.sessionID) return - navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`) - return - } - - if (!item.path) return - open(item.path) - } - - onCleanup(() => { - if (state.committed) return - state.cleanup?.() - }) - - if (filesOnly() && platform.platform === "desktop" && settings.general.newLayoutDesigns()) { - return ( - { - if (typeof result !== "string") return - open(result) - }} - /> - ) + const [files, nextSessions] = await Promise.all([ + palette.file.searchFiles(query), + Promise.resolve(palette.sessions(query)), + ]) + const category = palette.language.t("palette.group.files") + const entries = files.map((path) => createCommandPaletteFileEntry(path, category)) + return [...palette.commandEntries(), ...nextSessions, ...entries] } return ( @@ -411,21 +111,21 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil item.id} filterKeys={["title", "description", "category"]} skipFilter={(item) => item.type === "file"} groupBy={grouped() ? (item) => item.category : () => ""} - onMove={handleMove} - onSelect={handleSelect} + onMove={(item: CommandPaletteEntry | undefined) => palette.highlight(item)} + onSelect={(item: CommandPaletteEntry | undefined) => palette.select(item)} > {(item) => ( - {formatKeybind(item.keybind ?? "", language.t)} + {formatKeybind(item.keybind ?? "", palette.language.t)} @@ -479,7 +179,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil - {getRelativeTime(new Date(item.updated!).toISOString(), language.t)} + {getRelativeTime(new Date(item.updated!).toISOString(), palette.language.t)} diff --git a/packages/app/src/index.css b/packages/app/src/index.css index bc6866f976..78a9e103a7 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -224,7 +224,26 @@ [data-slot="model-selector-scroll"]::before { top: 0; - background: linear-gradient(to bottom, var(--v2-background-bg-layer-01), transparent); + height: 12px; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--v2-background-bg-layer-01) 80%, transparent) 0%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 78.3336%, transparent) 4.7%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 76.4824%, transparent) 8.9%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 74.368%, transparent) 12.8%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 71.9123%, transparent) 16.56%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 69.037%, transparent) 20.37%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 65.664%, transparent) 24.4%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 61.715%, transparent) 28.83%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 57.1117%, transparent) 33.84%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 51.776%, transparent) 39.6%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 45.6296%, transparent) 46.3%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 38.5944%, transparent) 54.1%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 30.592%, transparent) 63.2%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 21.5443%, transparent) 73.76%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 11.373%, transparent) 85.97%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 0%, transparent) 100% + ); } [data-slot="manage-models-scroll"]::before { @@ -239,7 +258,26 @@ [data-slot="model-selector-scroll"]::after { bottom: 0; - background: linear-gradient(to top, var(--v2-background-bg-layer-01), transparent); + height: 12px; + background: linear-gradient( + 0deg, + color-mix(in srgb, var(--v2-background-bg-layer-01) 80%, transparent) 0%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 78.3336%, transparent) 4.7%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 76.4824%, transparent) 8.9%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 74.368%, transparent) 12.8%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 71.9123%, transparent) 16.56%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 69.037%, transparent) 20.37%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 65.664%, transparent) 24.4%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 61.715%, transparent) 28.83%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 57.1117%, transparent) 33.84%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 51.776%, transparent) 39.6%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 45.6296%, transparent) 46.3%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 38.5944%, transparent) 54.1%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 30.592%, transparent) 63.2%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 21.5443%, transparent) 73.76%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 11.373%, transparent) 85.97%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 0%, transparent) 100% + ); } @supports (animation-timeline: --home-projects-scroll) and (timeline-scope: --home-projects-scroll) { diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index efbab7809e..762b8b2c8e 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -1278,7 +1278,7 @@ function HomeSessionSearch(props: { -