diff --git a/package-lock.json b/package-lock.json
index da7b1325..4435eea2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -36,6 +36,7 @@
"chokidar": "^4.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
"cors": "^2.8.5",
"cross-spawn": "^7.0.3",
"express": "^4.18.2",
@@ -3463,6 +3464,447 @@
"node": ">=14"
}
},
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
+ "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
+ "license": "MIT",
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@release-it/conventional-changelog": {
"version": "10.0.5",
"resolved": "https://registry.npmjs.org/@release-it/conventional-changelog/-/conventional-changelog-10.0.5.tgz",
@@ -4111,7 +4553,7 @@
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
@@ -5084,6 +5526,18 @@
"sprintf-js": "~1.0.2"
}
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/array-buffer-byte-length": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
@@ -6186,6 +6640,22 @@
"node": ">=6"
}
},
+ "node_modules/cmdk": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
+ "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.6",
+ "@radix-ui/react-id": "^1.1.0",
+ "@radix-ui/react-primitive": "^2.0.2"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
+ }
+ },
"node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
@@ -6958,6 +7428,12 @@
"node": ">=8"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
@@ -8674,6 +9150,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -14063,6 +14548,53 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-router": {
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
@@ -14095,6 +14627,28 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-syntax-highlighter": {
"version": "15.6.6",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz",
@@ -17592,6 +18146,49 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
diff --git a/package.json b/package.json
index ab77f41d..32033484 100644
--- a/package.json
+++ b/package.json
@@ -91,6 +91,7 @@
"chokidar": "^4.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
"cors": "^2.8.5",
"cross-spawn": "^7.0.3",
"express": "^4.18.2",
diff --git a/shared/modelConstants.js b/shared/modelConstants.js
index fd47010b..90b973ed 100644
--- a/shared/modelConstants.js
+++ b/shared/modelConstants.js
@@ -51,7 +51,7 @@ export const CURSOR_MODELS = {
{ value: "grok", label: "Grok" },
],
- DEFAULT: "gpt-5-3-codex",
+ DEFAULT: "gpt-5.3-codex",
};
/**
@@ -94,3 +94,13 @@ export const GEMINI_MODELS = {
DEFAULT: "gemini-3.1-pro-preview",
};
+
+/**
+ * Ordered provider registry. Display order in selection UIs.
+ */
+export const PROVIDERS = [
+ { id: "claude", name: "Anthropic", models: CLAUDE_MODELS },
+ { id: "codex", name: "OpenAI", models: CODEX_MODELS },
+ { id: "gemini", name: "Google", models: GEMINI_MODELS },
+ { id: "cursor", name: "Cursor", models: CURSOR_MODELS },
+];
diff --git a/src/components/app/AppContent.tsx b/src/components/app/AppContent.tsx
index 63c11df0..432447e4 100644
--- a/src/components/app/AppContent.tsx
+++ b/src/components/app/AppContent.tsx
@@ -1,14 +1,25 @@
import { useEffect, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
+
import Sidebar from '../sidebar/view/Sidebar';
import MainContent from '../main-content/view/MainContent';
+import CommandPalette from '../command-palette/CommandPalette';
import { useWebSocket } from '../../contexts/WebSocketContext';
+import { PaletteOpsProvider, usePaletteOpsRegister } from '../../contexts/PaletteOpsContext';
import { useDeviceSettings } from '../../hooks/useDeviceSettings';
import { useSessionProtection } from '../../hooks/useSessionProtection';
import { useProjectsState } from '../../hooks/useProjectsState';
export default function AppContent() {
+ return (
+
+
+
+ );
+}
+
+function AppContentInner() {
const navigate = useNavigate();
const { sessionId } = useParams<{ sessionId?: string }>();
const { t } = useTranslation('common');
@@ -40,6 +51,7 @@ export default function AppContent() {
openSettings,
refreshProjectsSilently,
sidebarSharedProps,
+ handleNewSession,
} = useProjectsState({
sessionId,
navigate,
@@ -48,27 +60,10 @@ export default function AppContent() {
activeSessions,
});
- useEffect(() => {
- // Expose a non-blocking refresh for chat/session flows.
- // Full loading refreshes are still available through direct fetchProjects calls.
- window.refreshProjects = refreshProjectsSilently;
-
- return () => {
- if (window.refreshProjects === refreshProjectsSilently) {
- delete window.refreshProjects;
- }
- };
- }, [refreshProjectsSilently]);
-
- useEffect(() => {
- window.openSettings = openSettings;
-
- return () => {
- if (window.openSettings === openSettings) {
- delete window.openSettings;
- }
- };
- }, [openSettings]);
+ usePaletteOpsRegister({
+ openSettings,
+ refreshProjects: refreshProjectsSilently,
+ });
useEffect(() => {
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
@@ -202,6 +197,12 @@ export default function AppContent() {
/>
+ openSettings()}
+ onShowTab={setActiveTab}
+ />
);
}
diff --git a/src/components/chat/hooks/useChatRealtimeHandlers.ts b/src/components/chat/hooks/useChatRealtimeHandlers.ts
index 73d1a5e7..855ee788 100644
--- a/src/components/chat/hooks/useChatRealtimeHandlers.ts
+++ b/src/components/chat/hooks/useChatRealtimeHandlers.ts
@@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react';
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
+import { usePaletteOps } from '../../../contexts/PaletteOpsContext';
import type { PendingPermissionRequest } from '../types/types';
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore';
@@ -99,6 +100,7 @@ export function useChatRealtimeHandlers({
onWebSocketReconnect,
sessionStore,
}: UseChatRealtimeHandlersArgs) {
+ const paletteOps = usePaletteOps();
const lastProcessedMessageRef = useRef(null);
useEffect(() => {
@@ -280,9 +282,7 @@ export function useChatRealtimeHandlers({
onNavigateToSession?.(actualId);
}
sessionStorage.removeItem('pendingSessionId');
- if (window.refreshProjects) {
- setTimeout(() => window.refreshProjects?.(), 500);
- }
+ setTimeout(() => { void paletteOps.refreshProjects(); }, 500);
}
break;
}
@@ -365,5 +365,6 @@ export function useChatRealtimeHandlers({
onNavigateToSession,
onWebSocketReconnect,
sessionStore,
+ paletteOps,
]);
}
diff --git a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
index 04569e60..3f82438f 100644
--- a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
+++ b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Check, ChevronDown } from "lucide-react";
-import { useTranslation } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import { useServerPlatform } from "../../../../hooks/useServerPlatform";
import SessionProviderLogo from "../../../llm-logo-provider/SessionProviderLogo";
@@ -9,6 +9,7 @@ import {
CURSOR_MODELS,
CODEX_MODELS,
GEMINI_MODELS,
+ PROVIDERS,
} from "../../../../../shared/modelConstants";
import type { ProjectSession, LLMProvider } from "../../../../types/app";
import { NextTaskBanner } from "../../../task-master";
@@ -26,6 +27,9 @@ import {
Card,
} from "../../../../shared/view/ui";
+const MOD_KEY =
+ typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘" : "Ctrl";
+
type ProviderSelectionEmptyStateProps = {
selectedSession: ProjectSession | null;
currentSessionId: string | null;
@@ -52,12 +56,11 @@ type ProviderGroup = {
models: { value: string; label: string }[];
};
-const PROVIDER_GROUPS: ProviderGroup[] = [
- { id: "claude", name: "Anthropic", models: CLAUDE_MODELS.OPTIONS },
- { id: "cursor", name: "Cursor", models: CURSOR_MODELS.OPTIONS },
- { id: "codex", name: "OpenAI", models: CODEX_MODELS.OPTIONS },
- { id: "gemini", name: "Google", models: GEMINI_MODELS.OPTIONS },
-];
+const PROVIDER_GROUPS: ProviderGroup[] = PROVIDERS.map((p) => ({
+ id: p.id as LLMProvider,
+ name: p.name,
+ models: p.models.OPTIONS,
+}));
function getModelConfig(p: LLMProvider) {
if (p === "claude") return CLAUDE_MODELS;
@@ -231,9 +234,14 @@ export default function ProviderSelectionEmptyState({
defaultValue: "No models found.",
})}
- {visibleProviderGroups.map((group) => (
+ {visibleProviderGroups.map((group, idx) => (
0
+ ? "border-t border-border/40 [&_[cmdk-group-heading]]:mt-1 [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-wider"
+ : "[&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-wider"
+ }
heading={
@@ -248,6 +256,7 @@ export default function ProviderSelectionEmptyState({
key={`${group.id}-${model.value}`}
value={`${group.name} ${model.label}`}
onSelect={() => handleModelSelect(group.id, model.value)}
+ className="ml-4 border-l border-border/40 pl-4"
>
{model.label}
{isSelected && (
@@ -282,6 +291,18 @@ export default function ProviderSelectionEmptyState({
}
+
+
+ ),
+ }}
+ />
+
+
{provider && tasksEnabled && isTaskMasterInstalled && (
setMarkdownPreview((previous) => !previous)}
- onOpenSettings={() => window.openSettings?.('appearance')}
+ onOpenSettings={() => paletteOps.openSettings('appearance')}
onDownload={handleDownload}
onSave={handleSave}
onToggleFullscreen={() => setIsFullscreen((previous) => !previous)}
diff --git a/src/components/command-palette/CommandPalette.tsx b/src/components/command-palette/CommandPalette.tsx
new file mode 100644
index 00000000..db3bd87b
--- /dev/null
+++ b/src/components/command-palette/CommandPalette.tsx
@@ -0,0 +1,373 @@
+import * as React from 'react';
+import { useNavigate } from 'react-router-dom';
+import {
+ ArrowDownToLine,
+ ArrowUpFromLine,
+ ChevronRight,
+ FileText,
+ GitCommit,
+ GitMerge,
+ MessageSquare,
+ MessageSquarePlus,
+ RefreshCw,
+ Settings,
+ SunMoon,
+ X,
+} from 'lucide-react';
+
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ Dialog,
+ DialogContent,
+ DialogTitle,
+} from '../../shared/view/ui';
+import { useTheme } from '../../contexts/ThemeContext';
+import { usePaletteOps } from '../../contexts/PaletteOpsContext';
+import { SETTINGS_MAIN_TABS } from '../settings/constants/constants';
+import type { AppTab, Project } from '../../types/app';
+
+import { useSessionsSource } from './sources/useSessionsSource';
+import { useFilesSource } from './sources/useFilesSource';
+import { useCommitsSource } from './sources/useCommitsSource';
+import { useSessionMessageSearch } from './sources/useSessionMessageSearch';
+import { useBranchesSource } from './sources/useBranchesSource';
+import { useGitActions } from './sources/useGitActions';
+
+type Page = 'actions' | 'files' | 'sessions' | 'commits' | 'branches';
+
+const PAGE_LABELS: Record = {
+ actions: 'Actions',
+ files: 'Files',
+ sessions: 'Sessions',
+ commits: 'Commits',
+ branches: 'Branches',
+};
+
+type CommandPaletteProps = {
+ selectedProject: Project | null;
+ onStartNewChat: (project: Project) => void;
+ onOpenSettings: (tab?: string) => void;
+ onShowTab?: (tab: AppTab) => void;
+};
+
+const NAV_TABS: Array<{ id: AppTab; label: string; keywords: string }> = [
+ { id: 'chat', label: 'Go to Chat', keywords: 'chat messages conversation' },
+ { id: 'files', label: 'Go to Files', keywords: 'files file tree explorer' },
+ { id: 'shell', label: 'Go to Shell', keywords: 'shell terminal console' },
+ { id: 'git', label: 'Go to Git', keywords: 'git diff branches' },
+ { id: 'tasks', label: 'Go to Tasks', keywords: 'tasks taskmaster' },
+];
+
+export default function CommandPalette({
+ selectedProject,
+ onStartNewChat,
+ onOpenSettings,
+ onShowTab,
+}: CommandPaletteProps) {
+ const [open, setOpen] = React.useState(false);
+ const [search, setSearch] = React.useState('');
+ const [pages, setPages] = React.useState([]);
+ const { toggleDarkMode } = useTheme();
+ const navigate = useNavigate();
+ const ops = usePaletteOps();
+
+ const page = pages.at(-1);
+
+ React.useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ const isCmdK = (e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'k';
+ if (!isCmdK) return;
+ e.preventDefault();
+ setOpen((prev) => !prev);
+ };
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, []);
+
+ React.useEffect(() => {
+ if (!open) {
+ setSearch('');
+ setPages([]);
+ }
+ }, [open]);
+
+ const projectId = selectedProject?.projectId;
+
+ const showActions = !page || page === 'actions';
+ const showSessions = !page || page === 'sessions';
+ const showFiles = !page || page === 'files';
+ const showCommits = !page || page === 'commits';
+ const showBranches = !page || page === 'branches' || page === 'actions';
+
+ const sessions = useSessionsSource(projectId, open && showSessions);
+ const messageMatches = useSessionMessageSearch(projectId, search, open && showSessions);
+ const files = useFilesSource(projectId, open && showFiles);
+ const commits = useCommitsSource(projectId, open && showCommits);
+ const branches = useBranchesSource(projectId, open && showBranches);
+ const git = useGitActions(projectId);
+
+ const sessionRows = React.useMemo(() => {
+ if (!showSessions) return [];
+ type Row = { id: string; label: string; provider?: string; snippet?: string };
+ const byId = new Map();
+ for (const s of sessions) {
+ byId.set(s.id, { id: s.id, label: s.label, provider: s.provider });
+ }
+ for (const m of messageMatches) {
+ const existing = byId.get(m.sessionId);
+ if (existing) {
+ existing.snippet = m.snippet;
+ } else {
+ byId.set(m.sessionId, {
+ id: m.sessionId,
+ label: m.label,
+ provider: m.provider,
+ snippet: m.snippet,
+ });
+ }
+ }
+ return Array.from(byId.values());
+ }, [sessions, messageMatches, showSessions]);
+
+ const run = React.useCallback((fn: () => void) => {
+ setOpen(false);
+ fn();
+ }, []);
+
+ const pushPage = React.useCallback((next: Page) => {
+ setSearch('');
+ setPages((prev) => [...prev, next]);
+ }, []);
+
+ const popPage = React.useCallback(() => {
+ setSearch('');
+ setPages((prev) => prev.slice(0, -1));
+ }, []);
+
+ const handleKeyDown = React.useCallback((e: React.KeyboardEvent) => {
+ if (e.key === 'Backspace' && !search && pages.length > 0) {
+ e.preventDefault();
+ popPage();
+ }
+ }, [search, pages.length, popPage]);
+
+ const startNewChatDisabled = !selectedProject;
+ const browseLimit = 5;
+ const filesShown = page === 'files' ? files : files.slice(0, browseLimit);
+ const commitsShown = page === 'commits' ? commits : commits.slice(0, browseLimit);
+ const sessionsShown = page === 'sessions' ? sessionRows : sessionRows.slice(0, browseLimit);
+ const branchesShown = page === 'branches' ? branches : branches.slice(0, browseLimit);
+
+ return (
+
+ );
+}
+
+function BrowseAllItem({ label, onSelect }: { label: string; onSelect: () => void }) {
+ return (
+
+
+ {label}
+
+ );
+}
diff --git a/src/components/command-palette/sources/useApiSource.ts b/src/components/command-palette/sources/useApiSource.ts
new file mode 100644
index 00000000..979a8914
--- /dev/null
+++ b/src/components/command-palette/sources/useApiSource.ts
@@ -0,0 +1,37 @@
+import { useEffect, useState, type DependencyList } from 'react';
+
+export function useApiSource(opts: {
+ enabled: boolean;
+ deps: DependencyList;
+ fetcher: (signal: AbortSignal) => Promise;
+ parse: (raw: R) => T[];
+}): T[] {
+ const [items, setItems] = useState([]);
+ const { enabled, deps, fetcher, parse } = opts;
+
+ useEffect(() => {
+ if (!enabled) {
+ setItems([]);
+ return;
+ }
+
+ const controller = new AbortController();
+
+ fetcher(controller.signal)
+ .then((r) => r.json() as Promise)
+ .then((data) => {
+ if (controller.signal.aborted) return;
+ setItems(parse(data));
+ })
+ .catch((err: unknown) => {
+ if (controller.signal.aborted) return;
+ if (err instanceof DOMException && err.name === 'AbortError') return;
+ setItems([]);
+ });
+
+ return () => controller.abort();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [enabled, ...deps]);
+
+ return items;
+}
diff --git a/src/components/command-palette/sources/useBranchesSource.ts b/src/components/command-palette/sources/useBranchesSource.ts
new file mode 100644
index 00000000..33a30870
--- /dev/null
+++ b/src/components/command-palette/sources/useBranchesSource.ts
@@ -0,0 +1,21 @@
+import { authenticatedFetch } from '../../../utils/api';
+
+import { useApiSource } from './useApiSource';
+
+export type BranchResult = { name: string };
+
+interface BranchesResponse {
+ localBranches?: string[];
+}
+
+export function useBranchesSource(projectId: string | undefined, enabled: boolean) {
+ return useApiSource({
+ enabled: enabled && !!projectId,
+ deps: [projectId],
+ fetcher: (signal) => {
+ const params = new URLSearchParams({ project: projectId! });
+ return authenticatedFetch(`/api/git/branches?${params.toString()}`, { signal });
+ },
+ parse: (data) => (data.localBranches ?? []).map((name) => ({ name })),
+ });
+}
diff --git a/src/components/command-palette/sources/useCommitsSource.ts b/src/components/command-palette/sources/useCommitsSource.ts
new file mode 100644
index 00000000..e173fa7f
--- /dev/null
+++ b/src/components/command-palette/sources/useCommitsSource.ts
@@ -0,0 +1,35 @@
+import { authenticatedFetch } from '../../../utils/api';
+
+import { useApiSource } from './useApiSource';
+
+export type CommitResult = {
+ hash: string;
+ shortHash: string;
+ message: string;
+ author: string;
+};
+
+interface CommitsResponse {
+ commits?: Array<{ hash: string; message: string; author: string }>;
+ error?: string;
+}
+
+export function useCommitsSource(projectId: string | undefined, enabled: boolean) {
+ return useApiSource({
+ enabled: enabled && !!projectId,
+ deps: [projectId],
+ fetcher: (signal) => {
+ const params = new URLSearchParams({ project: projectId!, limit: '50' });
+ return authenticatedFetch(`/api/git/commits?${params.toString()}`, { signal });
+ },
+ parse: (data) => {
+ if (!data.commits) return [];
+ return data.commits.map((c) => ({
+ hash: c.hash,
+ shortHash: c.hash.slice(0, 7),
+ message: c.message,
+ author: c.author,
+ }));
+ },
+ });
+}
diff --git a/src/components/command-palette/sources/useFilesSource.ts b/src/components/command-palette/sources/useFilesSource.ts
new file mode 100644
index 00000000..e96b511a
--- /dev/null
+++ b/src/components/command-palette/sources/useFilesSource.ts
@@ -0,0 +1,42 @@
+import { api } from '../../../utils/api';
+
+import { useApiSource } from './useApiSource';
+
+export type FileResult = {
+ path: string;
+ name: string;
+};
+
+interface FileNode {
+ type: 'file' | 'directory';
+ name: string;
+ path: string;
+ children?: FileNode[];
+}
+
+const MAX_FILES = 500;
+
+function flatten(nodes: FileNode[], out: FileResult[]): void {
+ for (const node of nodes) {
+ if (out.length >= MAX_FILES) return;
+ if (node.type === 'file') {
+ out.push({ path: node.path, name: node.name });
+ } else if (node.children && node.children.length > 0) {
+ flatten(node.children, out);
+ }
+ }
+}
+
+export function useFilesSource(projectId: string | undefined, enabled: boolean) {
+ return useApiSource({
+ enabled: enabled && !!projectId,
+ deps: [projectId],
+ fetcher: (signal) => api.getFiles(projectId!, { signal }),
+ parse: (data) => {
+ const tree: FileNode[] = Array.isArray(data) ? (data as FileNode[]) : [];
+ const flat: FileResult[] = [];
+ flatten(tree, flat);
+ return flat;
+ },
+ });
+}
diff --git a/src/components/command-palette/sources/useGitActions.ts b/src/components/command-palette/sources/useGitActions.ts
new file mode 100644
index 00000000..cf765f34
--- /dev/null
+++ b/src/components/command-palette/sources/useGitActions.ts
@@ -0,0 +1,38 @@
+import { useCallback } from 'react';
+
+import { authenticatedFetch } from '../../../utils/api';
+
+async function postGit(path: string, body: Record) {
+ const res = await authenticatedFetch(path, {
+ method: 'POST',
+ body: JSON.stringify(body),
+ });
+ return res.json();
+}
+
+export function useGitActions(projectId: string | undefined) {
+ const fetch = useCallback(() => {
+ if (!projectId) return Promise.resolve();
+ return postGit('/api/git/fetch', { project: projectId });
+ }, [projectId]);
+
+ const pull = useCallback(() => {
+ if (!projectId) return Promise.resolve();
+ return postGit('/api/git/pull', { project: projectId });
+ }, [projectId]);
+
+ const push = useCallback(() => {
+ if (!projectId) return Promise.resolve();
+ return postGit('/api/git/push', { project: projectId });
+ }, [projectId]);
+
+ const checkout = useCallback(
+ (branch: string) => {
+ if (!projectId) return Promise.resolve();
+ return postGit('/api/git/checkout', { project: projectId, branch });
+ },
+ [projectId],
+ );
+
+ return { fetch, pull, push, checkout };
+}
diff --git a/src/components/command-palette/sources/useSessionMessageSearch.ts b/src/components/command-palette/sources/useSessionMessageSearch.ts
new file mode 100644
index 00000000..f8a399eb
--- /dev/null
+++ b/src/components/command-palette/sources/useSessionMessageSearch.ts
@@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react';
+
+import { api } from '../../../utils/api';
+import type { LLMProvider } from '../../../types/app';
+
+export type SessionMessageMatch = {
+ sessionId: string;
+ label: string;
+ snippet: string;
+ provider: LLMProvider;
+};
+
+type ProjectResult = {
+ projectId: string | null;
+ projectName: string;
+ sessions: Array<{
+ sessionId: string;
+ provider: LLMProvider;
+ sessionSummary: string;
+ matches: Array<{ snippet: string }>;
+ }>;
+};
+
+const MIN_QUERY = 2;
+const DEBOUNCE_MS = 250;
+
+export function useSessionMessageSearch(
+ projectId: string | undefined,
+ query: string,
+ enabled: boolean,
+) {
+ const [items, setItems] = useState([]);
+ const seqRef = useRef(0);
+ const esRef = useRef(null);
+
+ useEffect(() => {
+ const trimmed = query.trim();
+ if (!enabled || !projectId || trimmed.length < MIN_QUERY) {
+ setItems([]);
+ esRef.current?.close();
+ esRef.current = null;
+ return;
+ }
+
+ esRef.current?.close();
+ esRef.current = null;
+ seqRef.current++;
+
+ const handle = setTimeout(() => {
+ const seq = ++seqRef.current;
+ const url = api.searchConversationsUrl(trimmed);
+ const es = new EventSource(url);
+ esRef.current = es;
+ const accumulated: SessionMessageMatch[] = [];
+
+ es.addEventListener('result', (evt) => {
+ if (seq !== seqRef.current) {
+ es.close();
+ return;
+ }
+ try {
+ const data = JSON.parse((evt as MessageEvent).data) as { projectResult: ProjectResult };
+ const pr = data.projectResult;
+ if (pr.projectId !== projectId) return;
+ for (const s of pr.sessions) {
+ accumulated.push({
+ sessionId: s.sessionId,
+ label: s.sessionSummary || s.sessionId,
+ snippet: s.matches[0]?.snippet ?? '',
+ provider: s.provider,
+ });
+ }
+ setItems([...accumulated]);
+ } catch {
+ // ignore malformed
+ }
+ });
+
+ const finish = () => {
+ if (seq !== seqRef.current) return;
+ es.close();
+ esRef.current = null;
+ };
+ es.addEventListener('done', finish);
+ es.addEventListener('error', finish);
+ }, DEBOUNCE_MS);
+
+ return () => {
+ clearTimeout(handle);
+ };
+ }, [projectId, query, enabled]);
+
+ useEffect(() => {
+ return () => {
+ esRef.current?.close();
+ esRef.current = null;
+ };
+ }, []);
+
+ return items;
+}
diff --git a/src/components/command-palette/sources/useSessionsSource.ts b/src/components/command-palette/sources/useSessionsSource.ts
new file mode 100644
index 00000000..9f20df58
--- /dev/null
+++ b/src/components/command-palette/sources/useSessionsSource.ts
@@ -0,0 +1,44 @@
+import { authenticatedFetch } from '../../../utils/api';
+import type { LLMProvider, ProjectSession } from '../../../types/app';
+
+import { useApiSource } from './useApiSource';
+
+export type SessionResult = {
+ id: string;
+ label: string;
+ provider?: LLMProvider;
+};
+
+interface SessionsResponse {
+ sessions?: ProjectSession[];
+ cursorSessions?: ProjectSession[];
+ codexSessions?: ProjectSession[];
+ geminiSessions?: ProjectSession[];
+}
+
+export function useSessionsSource(projectId: string | undefined, enabled: boolean) {
+ return useApiSource({
+ enabled: enabled && !!projectId,
+ deps: [projectId],
+ fetcher: (signal) => {
+ const params = new URLSearchParams({ limit: '50', offset: '0' });
+ return authenticatedFetch(
+ `/api/projects/${encodeURIComponent(projectId!)}/sessions?${params.toString()}`,
+ { signal },
+ );
+ },
+ parse: (data) => {
+ const all: ProjectSession[] = [
+ ...(data.sessions ?? []),
+ ...(data.cursorSessions ?? []),
+ ...(data.codexSessions ?? []),
+ ...(data.geminiSessions ?? []),
+ ];
+ return all.map((s) => ({
+ id: s.id,
+ label: (s.title || s.summary || s.name || s.id) as string,
+ provider: s.__provider,
+ }));
+ },
+ });
+}
diff --git a/src/components/main-content/view/MainContent.tsx b/src/components/main-content/view/MainContent.tsx
index bf0b87fc..a86dcbc9 100644
--- a/src/components/main-content/view/MainContent.tsx
+++ b/src/components/main-content/view/MainContent.tsx
@@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
+
import ChatInterface from '../../chat/view/ChatInterface';
import FileTree from '../../file-tree/view/FileTree';
import StandaloneShell from '../../standalone-shell/view/StandaloneShell';
@@ -6,12 +7,14 @@ import GitPanel from '../../git-panel/view/GitPanel';
import PluginTabContent from '../../plugins/view/PluginTabContent';
import type { MainContentProps } from '../types/types';
import { useTaskMaster } from '../../../contexts/TaskMasterContext';
+import { usePaletteOpsRegister } from '../../../contexts/PaletteOpsContext';
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
import { useUiPreferences } from '../../../hooks/useUiPreferences';
import { useEditorSidebar } from '../../code-editor/hooks/useEditorSidebar';
import EditorSidebar from '../../code-editor/view/EditorSidebar';
import type { Project } from '../../../types/app';
import { TaskMasterPanel } from '../../task-master';
+
import MainContentHeader from './subcomponents/MainContentHeader';
import MainContentStateView from './subcomponents/MainContentStateView';
import ErrorBoundary from './ErrorBoundary';
@@ -89,6 +92,13 @@ function MainContent({
}
}, [shouldShowTasksTab, activeTab, setActiveTab]);
+ usePaletteOpsRegister({
+ openFile: (filePath: string) => {
+ setActiveTab('files');
+ handleFileOpen(filePath);
+ },
+ });
+
if (isLoading) {
return ;
}
diff --git a/src/components/settings/constants/constants.ts b/src/components/settings/constants/constants.ts
index 429aabbe..e41da392 100644
--- a/src/components/settings/constants/constants.ts
+++ b/src/components/settings/constants/constants.ts
@@ -1,3 +1,15 @@
+import type { ComponentType } from 'react';
+import {
+ Bell,
+ Bot,
+ GitBranch,
+ Info,
+ KeyRound,
+ ListChecks,
+ Palette,
+ Plug,
+} from 'lucide-react';
+
import type {
AgentCategory,
AgentProvider,
@@ -7,13 +19,22 @@ import type {
SettingsMainTab,
} from '../types/types';
-export const SETTINGS_MAIN_TABS: SettingsMainTab[] = [
- 'agents',
- 'appearance',
- 'git',
- 'api',
- 'tasks',
- 'notifications',
+export type SettingsMainTabMeta = {
+ id: SettingsMainTab;
+ label: string;
+ keywords: string;
+ icon: ComponentType<{ className?: string }>;
+};
+
+export const SETTINGS_MAIN_TABS: SettingsMainTabMeta[] = [
+ { id: 'agents', label: 'Agents', keywords: 'agents subagents claude code', icon: Bot },
+ { id: 'appearance', label: 'Appearance', keywords: 'appearance theme dark light language', icon: Palette },
+ { id: 'git', label: 'Git', keywords: 'git github commits', icon: GitBranch },
+ { id: 'api', label: 'API Tokens', keywords: 'api tokens auth keys', icon: KeyRound },
+ { id: 'tasks', label: 'Tasks', keywords: 'tasks taskmaster', icon: ListChecks },
+ { id: 'notifications', label: 'Notifications', keywords: 'notifications alerts push', icon: Bell },
+ { id: 'plugins', label: 'Plugins', keywords: 'plugins extensions integrations', icon: Plug },
+ { id: 'about', label: 'About', keywords: 'about version info', icon: Info },
];
export const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex', 'gemini'];
@@ -34,4 +55,3 @@ export const DEFAULT_CURSOR_PERMISSIONS: CursorPermissionsState = {
disallowedCommands: [],
skipPermissions: false,
};
-
diff --git a/src/components/sidebar/hooks/useSidebarController.ts b/src/components/sidebar/hooks/useSidebarController.ts
index 514cf91b..d950bc43 100644
--- a/src/components/sidebar/hooks/useSidebarController.ts
+++ b/src/components/sidebar/hooks/useSidebarController.ts
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { TFunction } from 'i18next';
import { api } from '../../../utils/api';
+import { usePaletteOps } from '../../../contexts/PaletteOpsContext';
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
import type {
DeleteProjectConfirmation,
@@ -95,6 +96,7 @@ export function useSidebarController({
setSidebarVisible,
sidebarVisible,
}: UseSidebarControllerArgs) {
+ const paletteOps = usePaletteOps();
const [expandedProjects, setExpandedProjects] = useState>(new Set());
const [editingProject, setEditingProject] = useState(null);
const [showNewProject, setShowNewProject] = useState(false);
@@ -536,11 +538,7 @@ export function useSidebarController({
try {
const response = await api.renameProject(projectId, editingName);
if (response.ok) {
- if (window.refreshProjects) {
- await window.refreshProjects();
- } else {
- window.location.reload();
- }
+ await paletteOps.refreshProjects();
} else {
console.error('Failed to rename project');
}
@@ -551,7 +549,7 @@ export function useSidebarController({
setEditingName('');
}
},
- [editingName],
+ [editingName, paletteOps],
);
const showDeleteSessionConfirmation = useCallback(
diff --git a/src/components/sidebar/view/Sidebar.tsx b/src/components/sidebar/view/Sidebar.tsx
index 11a7dbe7..97484b01 100644
--- a/src/components/sidebar/view/Sidebar.tsx
+++ b/src/components/sidebar/view/Sidebar.tsx
@@ -6,6 +6,7 @@ import { useVersionCheck } from '../../../hooks/useVersionCheck';
import { useUiPreferences } from '../../../hooks/useUiPreferences';
import { useSidebarController } from '../hooks/useSidebarController';
import { useTaskMaster } from '../../../contexts/TaskMasterContext';
+import { usePaletteOps } from '../../../contexts/PaletteOpsContext';
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
import type { Project, LLMProvider } from '../../../types/app';
import type { MCPServerStatus, SidebarProps } from '../types/types';
@@ -49,6 +50,7 @@ function Sidebar({
const { sidebarVisible } = preferences;
const { setCurrentProject, mcpServerStatus } = useTaskMaster() as TaskMasterSidebarContext;
const { tasksEnabled } = useTasksSettings();
+ const paletteOps = usePaletteOps();
const {
isSidebarCollapsed,
@@ -128,12 +130,7 @@ function Sidebar({
}, [isPWA]);
const handleProjectCreated = () => {
- if (window.refreshProjects) {
- void window.refreshProjects();
- return;
- }
-
- window.location.reload();
+ void paletteOps.refreshProjects();
};
const projectListProps: SidebarProjectListProps = {
diff --git a/src/components/sidebar/view/subcomponents/SidebarHeader.tsx b/src/components/sidebar/view/subcomponents/SidebarHeader.tsx
index 551c0095..ab1eed7e 100644
--- a/src/components/sidebar/view/subcomponents/SidebarHeader.tsx
+++ b/src/components/sidebar/view/subcomponents/SidebarHeader.tsx
@@ -5,6 +5,9 @@ import { IS_PLATFORM } from '../../../../constants/config';
import { cn } from '../../../../lib/utils';
import GitHubStarBadge from './GitHubStarBadge';
+const MOD_KEY =
+ typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform) ? '⌘' : 'Ctrl';
+
type SearchMode = 'projects' | 'conversations';
type SidebarHeaderProps = {
@@ -148,9 +151,9 @@ export default function SidebarHeader({
placeholder={searchMode === 'conversations' ? t('search.conversationsPlaceholder') : t('projects.searchPlaceholder')}
value={searchFilter}
onChange={(event) => onSearchFilterChange(event.target.value)}
- className="nav-search-input h-9 rounded-xl border-0 pl-9 pr-8 text-sm transition-all duration-200 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0"
+ className="nav-search-input h-9 rounded-xl border-0 pl-9 pr-14 text-sm transition-all duration-200 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0"
/>
- {searchFilter && (
+ {searchFilter ? (
+ ) : (
+
+ {MOD_KEY}
+ K
+
)}
diff --git a/src/contexts/PaletteOpsContext.tsx b/src/contexts/PaletteOpsContext.tsx
new file mode 100644
index 00000000..c0281780
--- /dev/null
+++ b/src/contexts/PaletteOpsContext.tsx
@@ -0,0 +1,53 @@
+import { createContext, useContext, useEffect, useMemo, useRef } from 'react';
+import type { MutableRefObject, ReactNode } from 'react';
+
+export type PaletteOps = {
+ openFile: (path: string) => void;
+ openSettings: (tab?: string) => void;
+ refreshProjects: () => Promise | void;
+};
+
+type Registry = MutableRefObject>;
+
+const PaletteOpsContext = createContext(null);
+
+const defaultOps: PaletteOps = {
+ openFile: () => undefined,
+ openSettings: () => undefined,
+ refreshProjects: () => undefined,
+};
+
+export function PaletteOpsProvider({ children }: { children: ReactNode }) {
+ const ref = useRef>({});
+ return {children};
+}
+
+export function usePaletteOps(): PaletteOps {
+ const ref = useContext(PaletteOpsContext);
+ return useMemo(
+ () => ({
+ openFile: (path) => (ref?.current.openFile ?? defaultOps.openFile)(path),
+ openSettings: (tab) => (ref?.current.openSettings ?? defaultOps.openSettings)(tab),
+ refreshProjects: () => (ref?.current.refreshProjects ?? defaultOps.refreshProjects)(),
+ }),
+ [ref],
+ );
+}
+
+export function usePaletteOpsRegister(partial: Partial) {
+ const ref = useContext(PaletteOpsContext);
+ const { openFile, openSettings, refreshProjects } = partial;
+
+ useEffect(() => {
+ if (!ref) return undefined;
+ const prev = { ...ref.current };
+ if (openFile) ref.current.openFile = openFile;
+ if (openSettings) ref.current.openSettings = openSettings;
+ if (refreshProjects) ref.current.refreshProjects = refreshProjects;
+ return () => {
+ if (openFile && ref.current.openFile === openFile) ref.current.openFile = prev.openFile;
+ if (openSettings && ref.current.openSettings === openSettings) ref.current.openSettings = prev.openSettings;
+ if (refreshProjects && ref.current.refreshProjects === refreshProjects) ref.current.refreshProjects = prev.refreshProjects;
+ };
+ }, [ref, openFile, openSettings, refreshProjects]);
+}
diff --git a/src/i18n/locales/de/chat.json b/src/i18n/locales/de/chat.json
index 4d978bcf..b8a2d586 100644
--- a/src/i18n/locales/de/chat.json
+++ b/src/i18n/locales/de/chat.json
@@ -188,7 +188,8 @@
"codex": "Bereit, Codex mit {{model}} zu verwenden. Gib unten deine Nachricht ein.",
"gemini": "Bereit, Gemini mit {{model}} zu verwenden. Gib unten deine Nachricht ein.",
"default": "Wähl oben einen Anbieter, um zu beginnen"
- }
+ },
+ "pressToSearch": "Drücke {{shortcut}}, um Sitzungen, Dateien und Commits zu durchsuchen"
},
"session": {
"continue": {
diff --git a/src/i18n/locales/de/sidebar.json b/src/i18n/locales/de/sidebar.json
index b2df5190..ecc489ea 100644
--- a/src/i18n/locales/de/sidebar.json
+++ b/src/i18n/locales/de/sidebar.json
@@ -47,7 +47,8 @@
"deleteSession": "Diese Sitzung dauerhaft löschen",
"save": "Speichern",
"cancel": "Abbrechen",
- "clearSearch": "Suche leeren"
+ "clearSearch": "Suche leeren",
+ "openCommandPalette": "Befehlspalette öffnen"
},
"navigation": {
"chat": "Chat",
diff --git a/src/i18n/locales/en/chat.json b/src/i18n/locales/en/chat.json
index dadaea89..32e756a2 100644
--- a/src/i18n/locales/en/chat.json
+++ b/src/i18n/locales/en/chat.json
@@ -188,7 +188,8 @@
"codex": "Ready to use Codex with {{model}}. Start typing your message below.",
"gemini": "Ready to use Gemini with {{model}}. Start typing your message below.",
"default": "Select a provider above to begin"
- }
+ },
+ "pressToSearch": "Press {{shortcut}} to search sessions, files, and commits"
},
"session": {
"continue": {
diff --git a/src/i18n/locales/en/sidebar.json b/src/i18n/locales/en/sidebar.json
index 7b3452cb..43498ce7 100644
--- a/src/i18n/locales/en/sidebar.json
+++ b/src/i18n/locales/en/sidebar.json
@@ -47,7 +47,8 @@
"deleteSession": "Delete this session permanently",
"save": "Save",
"cancel": "Cancel",
- "clearSearch": "Clear search"
+ "clearSearch": "Clear search",
+ "openCommandPalette": "Open command palette"
},
"navigation": {
"chat": "Chat",
diff --git a/src/i18n/locales/it/chat.json b/src/i18n/locales/it/chat.json
index 78a7555a..06f18b08 100644
--- a/src/i18n/locales/it/chat.json
+++ b/src/i18n/locales/it/chat.json
@@ -188,7 +188,8 @@
"codex": "Pronto a usare Codex con {{model}}. Inizia a digitare il tuo messaggio qui sotto.",
"gemini": "Pronto a usare Gemini con {{model}}. Inizia a digitare il tuo messaggio qui sotto.",
"default": "Seleziona un provider sopra per iniziare"
- }
+ },
+ "pressToSearch": "Premi {{shortcut}} per cercare sessioni, file e commit"
},
"session": {
"continue": {
diff --git a/src/i18n/locales/it/sidebar.json b/src/i18n/locales/it/sidebar.json
index 79a71bad..986e5fd1 100644
--- a/src/i18n/locales/it/sidebar.json
+++ b/src/i18n/locales/it/sidebar.json
@@ -47,7 +47,8 @@
"deleteSession": "Elimina questa sessione permanentemente",
"save": "Salva",
"cancel": "Annulla",
- "clearSearch": "Cancella ricerca"
+ "clearSearch": "Cancella ricerca",
+ "openCommandPalette": "Apri tavolozza comandi"
},
"navigation": {
"chat": "Chat",
diff --git a/src/i18n/locales/ja/chat.json b/src/i18n/locales/ja/chat.json
index 10e03192..eb878aa6 100644
--- a/src/i18n/locales/ja/chat.json
+++ b/src/i18n/locales/ja/chat.json
@@ -165,7 +165,8 @@
"cursor": "{{model}}でCursorを使用する準備ができました。下にメッセージを入力してください。",
"codex": "{{model}}でCodexを使用する準備ができました。下にメッセージを入力してください。",
"default": "上からプロバイダーを選択して開始してください"
- }
+ },
+ "pressToSearch": "{{shortcut}} を押してセッション、ファイル、コミットを検索"
},
"session": {
"continue": {
diff --git a/src/i18n/locales/ja/sidebar.json b/src/i18n/locales/ja/sidebar.json
index 33c17399..98861878 100644
--- a/src/i18n/locales/ja/sidebar.json
+++ b/src/i18n/locales/ja/sidebar.json
@@ -46,7 +46,8 @@
"editSessionName": "セッション名を手動で編集",
"deleteSession": "このセッションを完全に削除",
"save": "保存",
- "cancel": "キャンセル"
+ "cancel": "キャンセル",
+ "openCommandPalette": "コマンドパレットを開く"
},
"navigation": {
"chat": "チャット",
diff --git a/src/i18n/locales/ko/chat.json b/src/i18n/locales/ko/chat.json
index aaf5b45c..bc4775b8 100644
--- a/src/i18n/locales/ko/chat.json
+++ b/src/i18n/locales/ko/chat.json
@@ -170,7 +170,8 @@
"codex": "{{model}} 모델로 Codex를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
"gemini": "{{model}} 모델로 Gemini를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
"default": "시작하려면 위에서 제공자를 선택하세요"
- }
+ },
+ "pressToSearch": "{{shortcut}}를 눌러 세션, 파일 및 커밋을 검색하세요"
},
"session": {
"continue": {
diff --git a/src/i18n/locales/ko/sidebar.json b/src/i18n/locales/ko/sidebar.json
index d7fcafaa..cd7e0289 100644
--- a/src/i18n/locales/ko/sidebar.json
+++ b/src/i18n/locales/ko/sidebar.json
@@ -46,7 +46,8 @@
"editSessionName": "세션 이름 직접 편집",
"deleteSession": "이 세션 영구 삭제",
"save": "저장",
- "cancel": "취소"
+ "cancel": "취소",
+ "openCommandPalette": "명령 팔레트 열기"
},
"navigation": {
"chat": "채팅",
diff --git a/src/i18n/locales/ru/chat.json b/src/i18n/locales/ru/chat.json
index a1c5e277..4fd1a0b4 100644
--- a/src/i18n/locales/ru/chat.json
+++ b/src/i18n/locales/ru/chat.json
@@ -188,7 +188,8 @@
"codex": "Готов использовать Codex с {{model}}. Начните вводить сообщение ниже.",
"gemini": "Готов использовать Gemini с {{model}}. Начните вводить сообщение ниже.",
"default": "Выберите провайдера выше для начала"
- }
+ },
+ "pressToSearch": "Нажмите {{shortcut}}, чтобы искать сессии, файлы и коммиты"
},
"session": {
"continue": {
diff --git a/src/i18n/locales/ru/sidebar.json b/src/i18n/locales/ru/sidebar.json
index 9af33d54..fee429b1 100644
--- a/src/i18n/locales/ru/sidebar.json
+++ b/src/i18n/locales/ru/sidebar.json
@@ -47,7 +47,8 @@
"deleteSession": "Удалить этот сеанс навсегда",
"save": "Сохранить",
"cancel": "Отмена",
- "clearSearch": "Очистить поиск"
+ "clearSearch": "Очистить поиск",
+ "openCommandPalette": "Открыть палитру команд"
},
"navigation": {
"chat": "Чат",
diff --git a/src/i18n/locales/tr/chat.json b/src/i18n/locales/tr/chat.json
index a866fcce..2f60b91c 100644
--- a/src/i18n/locales/tr/chat.json
+++ b/src/i18n/locales/tr/chat.json
@@ -188,7 +188,8 @@
"codex": "Codex'i {{model}} ile kullanmaya hazır. Mesajını aşağıya yazmaya başla.",
"gemini": "Gemini'yi {{model}} ile kullanmaya hazır. Mesajını aşağıya yazmaya başla.",
"default": "Başlamak için yukarıdan bir sağlayıcı seç"
- }
+ },
+ "pressToSearch": "Oturumlarda, dosyalarda ve commit'lerde arama yapmak için {{shortcut}} tuşlarına bas"
},
"session": {
"continue": {
diff --git a/src/i18n/locales/tr/sidebar.json b/src/i18n/locales/tr/sidebar.json
index b0ba4ad7..0640fca5 100644
--- a/src/i18n/locales/tr/sidebar.json
+++ b/src/i18n/locales/tr/sidebar.json
@@ -47,7 +47,8 @@
"deleteSession": "Bu oturumu kalıcı olarak sil",
"save": "Kaydet",
"cancel": "İptal",
- "clearSearch": "Aramayı temizle"
+ "clearSearch": "Aramayı temizle",
+ "openCommandPalette": "Komut paletini aç"
},
"navigation": {
"chat": "Sohbet",
diff --git a/src/i18n/locales/zh-CN/chat.json b/src/i18n/locales/zh-CN/chat.json
index 1d224cc7..a478f18a 100644
--- a/src/i18n/locales/zh-CN/chat.json
+++ b/src/i18n/locales/zh-CN/chat.json
@@ -170,7 +170,8 @@
"codex": "准备好使用带有 {{model}} 的 Codex。请在下方开始输入您的消息。",
"gemini": "准备好使用带有 {{model}} 的 Gemini。请在下方开始输入您的消息。",
"default": "请在上方选择一个提供者以开始"
- }
+ },
+ "pressToSearch": "按 {{shortcut}} 搜索会话、文件和提交"
},
"session": {
"continue": {
diff --git a/src/i18n/locales/zh-CN/sidebar.json b/src/i18n/locales/zh-CN/sidebar.json
index 85053d92..9de4ea83 100644
--- a/src/i18n/locales/zh-CN/sidebar.json
+++ b/src/i18n/locales/zh-CN/sidebar.json
@@ -47,7 +47,8 @@
"deleteSession": "永久删除此会话",
"save": "保存",
"cancel": "取消",
- "clearSearch": "清除搜索"
+ "clearSearch": "清除搜索",
+ "openCommandPalette": "打开命令面板"
},
"navigation": {
"chat": "聊天",
diff --git a/src/shared/view/ui/Command.tsx b/src/shared/view/ui/Command.tsx
index 5bdccdbb..a66fc17c 100644
--- a/src/shared/view/ui/Command.tsx
+++ b/src/shared/view/ui/Command.tsx
@@ -1,320 +1,107 @@
import * as React from 'react';
+import { Command as CommandPrimitive } from 'cmdk';
import { Search } from 'lucide-react';
import { cn } from '../../../lib/utils';
-/*
- * Lightweight command palette — inspired by cmdk but no external deps.
- *
- * Architecture:
- * - Command owns the search string and a flat list of registered item values.
- * - Items register via context on mount and deregister on unmount.
- * - Filtering, active index, and keyboard nav happen centrally in Command.
- * - Items read their "is visible" / "is active" state from context.
- */
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Command.displayName = CommandPrimitive.displayName;
-interface ItemEntry {
- id: string;
- value: string; // searchable text (lowercase)
- onSelect: () => void;
- element: HTMLElement | null;
-}
-
-interface CommandContextValue {
- search: string;
- setSearch: (value: string) => void;
- /** Set of visible item IDs after filtering (derived state, not a ref). */
- visibleIds: Set;
- activeId: string | null;
- setActiveId: (id: string | null) => void;
- register: (entry: ItemEntry) => void;
- unregister: (id: string) => void;
- updateEntry: (id: string, patch: Partial>) => void;
-}
-
-const CommandContext = React.createContext(null);
-
-function useCommand() {
- const ctx = React.useContext(CommandContext);
- if (!ctx) throw new Error('Command components must be used within ');
- return ctx;
-}
-
-/* ─── Command (root) ─────────────────────────────────────────────── */
-
-type CommandProps = React.HTMLAttributes;
-
-const Command = React.forwardRef(
- ({ className, children, ...props }, ref) => {
- const [search, setSearch] = React.useState('');
- const entriesRef = React.useRef