diff --git a/package-lock.json b/package-lock.json
index 2dbd251f..60d4f18f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,13 +32,14 @@
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.4",
- "@radix-ui/react-scroll-area": "^1.2.2",
+ "@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.1.2",
+ "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.2.1",
- "@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.3",
- "@radix-ui/react-tooltip": "^1.1.6",
+ "@radix-ui/react-tooltip": "^1.2.7",
"@tabler/icons-react": "^2.14.0",
"async-mutex": "^0.5.0",
"axios": "^1.3.4",
@@ -65,6 +66,7 @@
"react-dom": "^18.2.0",
"react-dropzone": "^14.3.5",
"react-markdown": "^9.0.1",
+ "react-resizable-panels": "^3.0.2",
"react-syntax-highlighter": "^15.5.0",
"sse": "github:mpetazzoni/sse.js",
"tailwind-merge": "^2.5.5",
@@ -5175,6 +5177,24 @@
}
}
},
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+ "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0"
+ },
+ "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-compose-refs": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
@@ -6254,6 +6274,24 @@
}
}
},
+ "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+ "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0"
+ },
+ "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-roving-focus": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
@@ -6374,20 +6412,20 @@
}
},
"node_modules/@radix-ui/react-scroll-area": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz",
- "integrity": "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==",
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz",
+ "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==",
"license": "MIT",
"dependencies": {
- "@radix-ui/number": "1.1.0",
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0"
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -6404,16 +6442,22 @@
}
}
},
- "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/number": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
- "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
- "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
+ "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": "*",
@@ -6425,13 +6469,44 @@
}
}
},
- "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
- "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
+ "node_modules/@radix-ui/react-scroll-area/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-scroll-area/node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "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-scroll-area/node_modules/@radix-ui/react-presence": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+ "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-slot": "1.1.1"
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -6448,14 +6523,49 @@
}
}
},
- "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
- "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+ "node_modules/@radix-ui/react-scroll-area/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-compose-refs": "1.1.1"
+ "@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-scroll-area/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-scroll-area/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"
@@ -6509,6 +6619,24 @@
}
}
},
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+ "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0"
+ },
+ "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-select/node_modules/@radix-ui/react-visually-hidden": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz",
@@ -6532,6 +6660,52 @@
}
}
},
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
+ "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.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-separator/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-slider": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz",
@@ -6566,12 +6740,12 @@
}
},
"node_modules/@radix-ui/react-slot": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
- "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+ "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.0"
+ "@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -6583,6 +6757,21 @@
}
}
},
+ "node_modules/@radix-ui/react-slot/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-switch": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.1.tgz",
@@ -6762,23 +6951,23 @@
}
},
"node_modules/@radix-ui/react-tooltip": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz",
- "integrity": "sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==",
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
+ "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-dismissable-layer": "1.1.3",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-popper": "1.2.1",
- "@radix-ui/react-portal": "1.1.3",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-slot": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "@radix-ui/react-visually-hidden": "1.1.1"
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.7",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -6796,18 +6985,18 @@
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
- "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
- "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.0.1"
+ "@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -6825,9 +7014,24 @@
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
- "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
+ "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-tooltip/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": "*",
@@ -6840,16 +7044,16 @@
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz",
- "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==",
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
+ "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-escape-keydown": "1.1.0"
+ "@radix-ui/primitive": "1.1.2",
+ "@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": "*",
@@ -6866,22 +7070,40 @@
}
}
},
+ "node_modules/@radix-ui/react-tooltip/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-tooltip/node_modules/@radix-ui/react-popper": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
- "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
+ "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
- "@radix-ui/react-arrow": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0",
- "@radix-ui/react-use-rect": "1.1.0",
- "@radix-ui/react-use-size": "1.1.0",
- "@radix-ui/rect": "1.1.0"
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -6899,13 +7121,37 @@
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
- "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
+ "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.0.1",
- "@radix-ui/react-use-layout-effect": "1.1.0"
+ "@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-tooltip/node_modules/@radix-ui/react-presence": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+ "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -6923,12 +7169,12 @@
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
- "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
+ "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.1.1"
+ "@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -6945,13 +7191,29 @@
}
}
},
- "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
+ "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
- "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+ "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-tooltip/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-compose-refs": "1.1.1"
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -6963,6 +7225,81 @@
}
}
},
+ "node_modules/@radix-ui/react-tooltip/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-tooltip/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/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "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-tooltip/node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "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-tooltip/node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
@@ -6994,6 +7331,39 @@
}
}
},
+ "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-effect-event/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/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
@@ -7075,12 +7445,12 @@
}
},
"node_modules/@radix-ui/react-visually-hidden": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz",
- "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.0.1"
+ "@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -7097,28 +7467,13 @@
}
}
},
- "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
- "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
- "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-visually-hidden/node_modules/@radix-ui/react-primitive": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
- "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
+ "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.1.1"
+ "@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -7135,24 +7490,6 @@
}
}
},
- "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
- "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "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/rect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
@@ -18688,6 +19025,16 @@
}
}
},
+ "node_modules/react-resizable-panels": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.2.tgz",
+ "integrity": "sha512-j4RNII75fnHkLnbsTb5G5YsDvJsSEZrJK2XSF2z0Tc2jIonYlIVir/Yh/5LvcUFCfs1HqrMAoiBFmIrRjC4XnA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
diff --git a/package.json b/package.json
index 1f6ec86e..e7edf344 100644
--- a/package.json
+++ b/package.json
@@ -99,13 +99,14 @@
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.4",
- "@radix-ui/react-scroll-area": "^1.2.2",
+ "@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.1.2",
+ "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.2.1",
- "@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.3",
- "@radix-ui/react-tooltip": "^1.1.6",
+ "@radix-ui/react-tooltip": "^1.2.7",
"@tabler/icons-react": "^2.14.0",
"async-mutex": "^0.5.0",
"axios": "^1.3.4",
@@ -132,6 +133,7 @@
"react-dom": "^18.2.0",
"react-dropzone": "^14.3.5",
"react-markdown": "^9.0.1",
+ "react-resizable-panels": "^3.0.2",
"react-syntax-highlighter": "^15.5.0",
"sse": "github:mpetazzoni/sse.js",
"tailwind-merge": "^2.5.5",
diff --git a/src/components/chat-components/ProjectList.tsx b/src/components/chat-components/ProjectList.tsx
index 50f89d3a..8f4d982f 100644
--- a/src/components/chat-components/ProjectList.tsx
+++ b/src/components/chat-components/ProjectList.tsx
@@ -1,5 +1,5 @@
import { ProjectConfig, setCurrentProject } from "@/aiParams";
-import { AddProjectModal } from "@/components/modals/AddProjectModal";
+import { AddProjectModal } from "@/components/modals/project/AddProjectModal";
import { ConfirmModal } from "@/components/modals/ConfirmModal";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
@@ -11,6 +11,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import { SearchBar } from "@/components/ui/SearchBar";
import { cn } from "@/lib/utils";
import { logError } from "@/logger";
import { updateSetting } from "@/settings/model";
@@ -25,12 +26,10 @@ import {
Search,
Trash2,
X,
- XCircle,
} from "lucide-react";
import { App, Notice } from "obsidian";
import React, { memo, useEffect, useMemo, useState } from "react";
import { filterProjects } from "@/utils/projectUtils";
-import { Input } from "../ui/input";
function ProjectItem({
project,
@@ -356,23 +355,11 @@ export const ProjectList = memo(
{projects.length > 0 && (
)}
diff --git a/src/components/modals/ExtensionInputModal.tsx b/src/components/modals/ExtensionInputModal.tsx
index c82a2b00..316dbcf9 100644
--- a/src/components/modals/ExtensionInputModal.tsx
+++ b/src/components/modals/ExtensionInputModal.tsx
@@ -25,6 +25,8 @@ function ExtensionInputModalContent({
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
+ e.preventDefault();
+ e.stopPropagation();
validateAndConfirm(extension);
}
};
diff --git a/src/components/modals/PatternMatchingModal.tsx b/src/components/modals/PatternMatchingModal.tsx
index ce3d208b..782fd917 100644
--- a/src/components/modals/PatternMatchingModal.tsx
+++ b/src/components/modals/PatternMatchingModal.tsx
@@ -4,8 +4,11 @@ import React, { useState } from "react";
import { createRoot, Root } from "react-dom/client";
import {
categorizePatterns,
- getDecodedPatterns,
createPatternSettingsValue,
+ getDecodedPatterns,
+ getExtensionPattern,
+ getFilePattern,
+ getTagPattern,
} from "@/search/searchUtils";
import { File, FileText, Folder, Tag, Wrench, X } from "lucide-react";
import {
@@ -148,7 +151,7 @@ function PatternMatchingModalContent({
{
new TagSearchModal(app, (tag) => {
- const tagPattern = `#${tag}`;
+ const tagPattern = getTagPattern(tag);
if (tagPatterns.includes(tagPattern)) {
return;
}
@@ -185,7 +188,7 @@ function PatternMatchingModalContent({
new AddContextNoteModal({
app,
onNoteSelect: (note) => {
- const notePattern = `[[${note.basename}]]`;
+ const notePattern = getFilePattern(note);
if (notePatterns.includes(notePattern)) {
return;
}
@@ -206,7 +209,7 @@ function PatternMatchingModalContent({
{
new ExtensionInputModal(app, (extension) => {
- const extensionPattern = `*.${extension}`;
+ const extensionPattern = getExtensionPattern(extension);
if (extensionPatterns.includes(extensionPattern)) {
return;
}
diff --git a/src/components/modals/ProjectPatternMatchingModal.tsx b/src/components/modals/ProjectPatternMatchingModal.tsx
index e525fd08..288c43b2 100644
--- a/src/components/modals/ProjectPatternMatchingModal.tsx
+++ b/src/components/modals/ProjectPatternMatchingModal.tsx
@@ -10,6 +10,9 @@ import {
categorizePatterns,
createPatternSettingsValue,
getDecodedPatterns,
+ getExtensionPattern,
+ getFilePattern,
+ getTagPattern,
} from "@/search/searchUtils";
import { File, FileText, Folder, Tag, Wrench, X } from "lucide-react";
import { App, Modal, TFile } from "obsidian";
@@ -148,7 +151,7 @@ function ProjectPatternMatchingModalContent({
{
new TagSearchModal(app, (tag) => {
- const tagPattern = `#${tag}`;
+ const tagPattern = getTagPattern(tag);
if (tagPatterns.includes(tagPattern)) {
return;
}
@@ -185,7 +188,7 @@ function ProjectPatternMatchingModalContent({
new ProjectFileSelectModal({
app,
onFileSelect: (file: TFile) => {
- const filePattern = `[[${file.basename}]]`;
+ const filePattern = getFilePattern(file);
if (notePatterns.includes(filePattern)) {
return;
}
@@ -206,7 +209,7 @@ function ProjectPatternMatchingModalContent({
{
new ExtensionInputModal(app, (extension) => {
- const extensionPattern = `*.${extension}`;
+ const extensionPattern = getExtensionPattern(extension);
if (extensionPatterns.includes(extensionPattern)) {
return;
}
diff --git a/src/components/modals/AddProjectModal.tsx b/src/components/modals/project/AddProjectModal.tsx
similarity index 86%
rename from src/components/modals/AddProjectModal.tsx
rename to src/components/modals/project/AddProjectModal.tsx
index c1cb355e..898ed111 100644
--- a/src/components/modals/AddProjectModal.tsx
+++ b/src/components/modals/project/AddProjectModal.tsx
@@ -1,5 +1,4 @@
import { ProjectConfig } from "@/aiParams";
-import { ProjectPatternMatchingModal } from "@/components/modals/ProjectPatternMatchingModal";
import { Button } from "@/components/ui/button";
import { FormField } from "@/components/ui/form-field";
import { Input } from "@/components/ui/input";
@@ -14,6 +13,9 @@ import { HelpCircle } from "lucide-react";
import { App, Modal, Notice } from "obsidian";
import React, { useState } from "react";
import { createRoot, Root } from "react-dom/client";
+import { ContextManageModal } from "@/components/modals/project/context-manage-modal";
+import { TruncatedText } from "@/components/TruncatedText";
+import { getDecodedPatterns } from "@/search/searchUtils";
interface AddProjectModalContentProps {
initialProject?: ProjectConfig;
@@ -31,7 +33,7 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
inclusions: false,
});
- const [formData, setFormData] = useState>(
+ const [formData, setFormData] = useState(
initialProject || {
id: randomUUID(),
name: "",
@@ -53,6 +55,23 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
}
);
+ const showContext = getDecodedPatterns(
+ formData.contextSource.inclusions || formData.contextSource.exclusions || "nothing"
+ )
+ .reverse()
+ .join(",");
+
+ const handleEditProjectContext = (originP: ProjectConfig) => {
+ const modal = new ContextManageModal(
+ app,
+ async (updatedProject: ProjectConfig) => {
+ setFormData(updatedProject);
+ },
+ originP
+ );
+ modal.open();
+ };
+
const isFormValid = () => {
return formData.name && formData.projectModelKey;
};
@@ -130,7 +149,9 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
return (
-
Add New Project
+
+ {initialProject ? "Edit Project" : "New Project"}
+
- Inclusions
+ Project Context
@@ -261,53 +282,19 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
description="Define patterns to include specific files or folders in the project context"
>
-
- {formData.contextSource?.inclusions?.trim()
- ? "Patterns configured"
- : "No patterns configured"}
+
+ Patterns configure:
+
+ {showContext}
+
- new ProjectPatternMatchingModal(
- app,
- (value: string) => {
- handleInputChange("contextSource.inclusions", value);
- },
- formData.contextSource?.inclusions || "",
- "Manage Inclusions"
- ).open()
- }
+ onClick={() => {
+ handleEditProjectContext(formData);
+ }}
>
- Manage Patterns
-
-
-
-
-
-
-
- {formData.contextSource?.exclusions?.trim()
- ? "Patterns configured"
- : "No patterns configured"}
-
-
- new ProjectPatternMatchingModal(
- app,
- (value: string) => {
- handleInputChange("contextSource.exclusions", value);
- },
- formData.contextSource?.exclusions || "",
- "Manage Exclusions"
- ).open()
- }
- >
- Manage Patterns
+ Manage Context
diff --git a/src/components/modals/project/context-manage-modal.tsx b/src/components/modals/project/context-manage-modal.tsx
new file mode 100644
index 00000000..c51507ff
--- /dev/null
+++ b/src/components/modals/project/context-manage-modal.tsx
@@ -0,0 +1,1059 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import {
+ FileAudio,
+ FileImage,
+ FileText,
+ FileVideo,
+ FolderIcon,
+ Plus,
+ PlusCircle,
+ TagIcon,
+ XIcon,
+} from "lucide-react";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
+import { Separator } from "@/components/ui/separator";
+import { cn } from "@/lib/utils";
+import { App, Modal, Notice, TFile } from "obsidian";
+import { createRoot, Root } from "react-dom/client";
+import { ProjectConfig } from "@/aiParams";
+import { Button } from "@/components/ui/button";
+import { SearchBar } from "@/components/ui/SearchBar";
+import { TagSearchModal } from "@/components/modals/TagSearchModal";
+import { FolderSearchModal } from "@/components/modals/FolderSearchModal";
+import { ProjectFileSelectModal } from "@/components/modals/ProjectFileSelectModal";
+import {
+ createPatternSettingsValue,
+ getFilePattern,
+ getMatchingPatterns,
+ getTagPattern,
+ PatternCategory,
+ shouldIndexFile,
+} from "@/search/searchUtils";
+import { getTagsFromNote } from "@/utils";
+import { TruncatedText } from "@/components/TruncatedText";
+
+function FileIcon({ extension, size = "tw-size-4" }: { extension: string; size?: string }) {
+ const ext = extension.toLowerCase().replace("*.", "");
+ if (["jpg", "jpeg", "png", "gif", "svg"].includes(ext))
+ return
;
+ if (["mp3", "wav", "ogg"].includes(ext))
+ return
;
+ if (["mp4", "mov", "avi"].includes(ext))
+ return
;
+ if (["md", "txt", "mdx", "markdown"].includes(ext)) return
;
+
+ return
;
+}
+
+interface ParsedQuery {
+ tags: string[];
+ titles: string[];
+ extensions: string[];
+}
+
+type ActiveSection = "tags" | "folders" | "files" | "extensions" | "ignoreFiles" | "search" | null;
+type ActiveItem = string | null;
+
+interface SectionHeaderProps {
+ IconComponent: React.ComponentType
;
+ title: string;
+ iconColorClassName: string;
+ onAddClick: () => void;
+}
+
+const SectionHeader: React.FC = ({
+ IconComponent,
+ title,
+ iconColorClassName,
+ onAddClick,
+}) => {
+ return (
+
+ );
+};
+
+interface SectionItem {
+ id: string;
+ name: string;
+ count?: number;
+}
+
+interface SectionListProps {
+ title: string;
+ IconComponent: React.ComponentType;
+ iconColorClassName: string;
+ items: SectionItem[];
+ activeItem: string | null;
+ activeSection: string | null;
+ sectionType: string;
+ renderIcon?: (item: SectionItem) => React.ReactNode;
+ onItemClick: (itemId: string, itemName?: string) => void;
+ onAddClick: () => void;
+ onDeleteItem: (e: React.MouseEvent, item: SectionItem) => void;
+}
+
+const SectionList: React.FC = ({
+ title,
+ IconComponent,
+ iconColorClassName,
+ items,
+ activeItem,
+ activeSection,
+ sectionType,
+ renderIcon,
+ onItemClick,
+ onAddClick,
+ onDeleteItem,
+}) => {
+ return (
+
+
+
+ {items.map((item) => (
+
onItemClick(item.id, item.name)}
+ >
+
+ {renderIcon ? (
+ renderIcon(item)
+ ) : (
+
+ )}
+ {item.name}
+
+
+
+ {item.count || 0}
+
+ onDeleteItem(e, item)}
+ />
+
+
+ ))}
+
+
+ );
+};
+
+interface ItemCardProps {
+ item: GroupItem;
+ viewMode: "list";
+ onDelete: (e: React.MouseEvent, item: GroupItem) => void;
+}
+
+function ItemCard({ item, viewMode, onDelete }: ItemCardProps) {
+ const extension = item.id.split(".").pop() || "";
+
+ // add or remove
+ const IconComponent = item.isIgnored ? Plus : XIcon;
+
+ return (
+
+
+
+
+
+
+ {item.isIgnored && (Ignored files) }
+ {item.name}
+
+ {item.id && (
+ {item.id}
+ )}
+
+
+
+ onDelete(e, item)}
+ />
+
+
+ );
+}
+
+interface ContextManageProps {
+ initialProject: ProjectConfig;
+ onSave: (project: ProjectConfig) => void;
+ onCancel: () => void;
+ app: App;
+}
+
+interface GroupItem {
+ id: string;
+ name: string;
+ isIgnored?: boolean;
+}
+
+interface GroupListItem {
+ tags: Record>;
+ folders: Record>;
+ extensions: Record>;
+ notes: Array;
+}
+
+interface IgnoreItems {
+ files: Set;
+}
+
+function ContextManage({ initialProject, onSave, onCancel, app }: ContextManageProps) {
+ const { inclusions: inclusionPatterns, exclusions: exclusionPatterns } = useMemo(() => {
+ return getMatchingPatterns({
+ inclusions: initialProject?.contextSource.inclusions,
+ exclusions: initialProject?.contextSource.exclusions,
+ isProject: true,
+ });
+ }, [initialProject.contextSource]);
+
+ const appAllFiles = useMemo(() => {
+ return app.vault.getFiles();
+ }, [app.vault]);
+
+ // init groupList
+ const createAndPopulateGroupList = useCallback(
+ (
+ appFiles: TFile[],
+ inclusionPatterns: PatternCategory | null,
+ exclusionPatterns: PatternCategory | null
+ ): GroupListItem => {
+ const projectAllFiles = appFiles.filter((file) =>
+ shouldIndexFile(file, inclusionPatterns, exclusionPatterns)
+ );
+
+ const processPatternGroup = (
+ file: TFile,
+ patterns: string[] | undefined,
+ patternType: "tagPatterns" | "folderPatterns" | "extensionPatterns",
+ targetGroup: Record>
+ ) => {
+ if (patterns) {
+ patterns.forEach((pattern) => {
+ const singlePatternConfig = { [patternType]: [pattern] };
+ if (
+ shouldIndexFile(file, singlePatternConfig, null) &&
+ !targetGroup[pattern].some((item) => item.id === file.path)
+ ) {
+ targetGroup[pattern].push({
+ id: file.path,
+ name: file.basename,
+ });
+ }
+ });
+ }
+ };
+
+ // initialize groups
+ const tags: Record> = {};
+ const folders: Record> = {};
+ const extensions: Record> = {};
+ const notes: Array = [];
+
+ (inclusionPatterns?.tagPatterns ?? []).forEach((tag) => {
+ tags[tag] = [];
+ });
+ (inclusionPatterns?.folderPatterns ?? []).forEach((folder) => {
+ folders[folder] = [];
+ });
+ (inclusionPatterns?.extensionPatterns ?? []).forEach((extension) => {
+ extensions[extension] = [];
+ });
+
+ // Traverse the files and populate them into corresponding groups
+ projectAllFiles.forEach((file) => {
+ // tag
+ processPatternGroup(file, inclusionPatterns?.tagPatterns, "tagPatterns", tags);
+
+ // folder
+ processPatternGroup(file, inclusionPatterns?.folderPatterns, "folderPatterns", folders);
+
+ // extension
+ processPatternGroup(
+ file,
+ inclusionPatterns?.extensionPatterns,
+ "extensionPatterns",
+ extensions
+ );
+
+ // note/file
+ if (
+ inclusionPatterns?.notePatterns &&
+ shouldIndexFile(file, { notePatterns: inclusionPatterns.notePatterns }, null) &&
+ !notes.some((item) => item.id === file.path)
+ ) {
+ notes.push({
+ id: file.path,
+ name: file.basename,
+ });
+ }
+ });
+
+ return {
+ tags,
+ folders,
+ extensions,
+ notes,
+ };
+ },
+ []
+ );
+
+ const [groupList, setGroupList] = useState(() => {
+ // init include files
+ return createAndPopulateGroupList(appAllFiles, inclusionPatterns, exclusionPatterns);
+ });
+ const [ignoreItems, setIgnoreItems] = useState(() => {
+ // init exclude files
+ const excludeFiles = appAllFiles.filter(
+ (file) => exclusionPatterns && shouldIndexFile(file, exclusionPatterns, null)
+ );
+ return {
+ files: new Set(excludeFiles),
+ };
+ });
+
+ const latestGroupList = useRef(groupList);
+ const latestIgnoreItems = useRef(ignoreItems);
+
+ const [searchTerm, setSearchTerm] = useState("");
+ const [activeSection, setActiveSection] = useState(null);
+ const [activeItem, setActiveItem] = useState(null);
+
+ // groupList convert to inclusions format
+ const convertGroupListToInclusions = useCallback(
+ (list: GroupListItem, appFiles: TFile[]): string => {
+ const tagPatterns = Object.keys(list.tags);
+ const folderPatterns = Object.keys(list.folders);
+ const extensionPatterns = Object.keys(list.extensions);
+ const notePatterns = list.notes
+ .map((note) => {
+ const file = appFiles.find((file) => file.path === note.id);
+ if (file) {
+ return getFilePattern(file);
+ }
+ })
+ .filter(Boolean) as string[];
+
+ return createPatternSettingsValue({
+ tagPatterns,
+ folderPatterns,
+ extensionPatterns,
+ notePatterns,
+ });
+ },
+ []
+ );
+
+ // ignore file items convert to exclusions format
+ const convertDeletedItemsToExclusions = useCallback((items: IgnoreItems): string => {
+ const notePatterns = new Array(...items.files).map((file) => getFilePattern(file));
+
+ return createPatternSettingsValue({ notePatterns: notePatterns }) || "";
+ }, []);
+
+ useEffect(() => {
+ latestGroupList.current = groupList;
+ latestIgnoreItems.current = ignoreItems;
+ }, [groupList, ignoreItems]);
+
+ const allItems: Array<{ id: string; name: string }> = useMemo(() => {
+ const items: Array<{ id: string; name: string }> = [];
+
+ const addFilesToItems = (
+ items: Array<{ id: string; name: string }>,
+ groupItems: GroupItem[]
+ ): void => {
+ groupItems.forEach((groupItem) => {
+ if (!items.some((item) => item.id === groupItem.id)) {
+ items.push({
+ id: groupItem.id,
+ name: groupItem.name,
+ });
+ }
+ });
+ };
+
+ const arr = [
+ groupList.tags,
+ groupList.folders,
+ groupList.extensions,
+ { notes: groupList.notes },
+ ];
+
+ arr.forEach((item) => {
+ Object.values(item).forEach((groupItems) => {
+ addFilesToItems(items, groupItems);
+ });
+ });
+
+ return items;
+ }, [groupList]);
+
+ const parseSearchQuery = useCallback((query: string): ParsedQuery => {
+ const tags: string[] = [];
+ const titles: string[] = [];
+ const extensions: string[] = [];
+
+ const parts = query
+ .split(",")
+ .map((p) => p.trim())
+ .filter((p) => p);
+
+ parts.forEach((part) => {
+ if (part.startsWith("#")) {
+ // tag
+ tags.push(part.substring(1));
+ } else if (part.startsWith(".") || part.startsWith("*.")) {
+ extensions.push(part.replace("*", ""));
+ } else {
+ // All other content is matched as title.
+ titles.push(part);
+ }
+ });
+ return { tags, titles, extensions };
+ }, []);
+
+ const getDisplayItems = useMemo(() => {
+ if (searchTerm) {
+ // Custom search
+ const parsedQuery = parseSearchQuery(searchTerm);
+ return allItems
+ .filter((item) => {
+ const fileObj = appAllFiles.find((f) => f.path === item.id);
+ if (!fileObj) return false;
+
+ const isNote = fileObj.extension === "md";
+
+ const matchesTag =
+ parsedQuery.tags.length > 0 &&
+ isNote &&
+ parsedQuery.tags.some((queryTag) => {
+ const fileTags = getTagsFromNote(fileObj);
+ return fileTags.some((tag) => {
+ const cleanTag = tag.startsWith("#") ? tag.substring(1) : tag;
+ return cleanTag.toLowerCase().includes(queryTag.toLowerCase());
+ });
+ });
+
+ const matchesTitle =
+ parsedQuery.titles.length > 0 &&
+ parsedQuery.titles.some((t) =>
+ fileObj.basename.toLowerCase().includes(t.toLowerCase())
+ );
+
+ const matchesExtension =
+ parsedQuery.extensions.length > 0 &&
+ parsedQuery.extensions.some(
+ (ext) => `.${fileObj.extension}`.toLowerCase() === ext.toLowerCase()
+ );
+
+ const hasSpecificFilters =
+ parsedQuery.tags.length > 0 ||
+ parsedQuery.titles.length > 0 ||
+ parsedQuery.extensions.length > 0;
+
+ if (hasSpecificFilters) {
+ return matchesTag || matchesTitle || matchesExtension;
+ }
+ return false;
+ })
+ .map((item) => {
+ return {
+ id: item.id,
+ name: item.name,
+ };
+ });
+ }
+
+ if (activeSection === "tags" && activeItem) {
+ const tagFiles = groupList.tags[activeItem];
+ if (tagFiles) {
+ return tagFiles;
+ }
+ return [];
+ }
+
+ if (activeSection === "folders" && activeItem) {
+ const folderFiles = groupList.folders[activeItem];
+ if (folderFiles) {
+ return folderFiles;
+ }
+ return [];
+ }
+
+ if (activeSection === "files") {
+ return groupList.notes;
+ }
+
+ if (activeSection === "extensions" && activeItem) {
+ const extensionFiles = groupList.extensions[activeItem];
+ if (extensionFiles) {
+ return extensionFiles;
+ }
+ return [];
+ }
+
+ if (activeSection === "ignoreFiles") {
+ return Array.from(ignoreItems.files).map((file) => ({
+ id: file.path,
+ name: file.basename,
+ }));
+ }
+
+ return [];
+ }, [
+ searchTerm,
+ activeSection,
+ activeItem,
+ parseSearchQuery,
+ allItems,
+ appAllFiles,
+ groupList.tags,
+ groupList.folders,
+ groupList.notes,
+ groupList.extensions,
+ ignoreItems.files,
+ ]);
+
+ const sortItems = useCallback((items: GroupItem[]) => {
+ return [...items].sort((a, b) => {
+ return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+ });
+ }, []);
+
+ const makeSectionItem = useCallback(
+ (
+ groupData: Record>,
+ nameTransform?: (name: string) => string
+ ): SectionItem[] => {
+ return Object.entries(groupData)
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([itemName, itemFiles]) => ({
+ id: itemName,
+ name: nameTransform ? nameTransform(itemName) : itemName,
+ count: itemFiles.length,
+ }));
+ },
+ []
+ );
+
+ const addPatternToGroup = useCallback(
+ (
+ groupType: "tags" | "folders" | "extensions",
+ pattern: string,
+ patternConfig: PatternCategory
+ ) => {
+ const getMatchingFilesFromApp = (patterns: PatternCategory): GroupItem[] => {
+ return appAllFiles
+ .filter((file) => shouldIndexFile(file, patterns, null))
+ .map((file) => ({
+ id: file.path,
+ name: file.basename,
+ }));
+ };
+
+ const ignoreFiles = [...latestIgnoreItems.current.files];
+ const matchingFiles: GroupItem[] = getMatchingFilesFromApp(patternConfig).map((v) => ({
+ ...v,
+ // add flag if file is ignored
+ isIgnored: ignoreFiles.some((f) => f.path === v.id),
+ }));
+
+ setGroupList((prev) => ({
+ ...prev,
+ [groupType]: {
+ ...prev[groupType],
+ [pattern]: matchingFiles,
+ },
+ }));
+ },
+ [appAllFiles]
+ );
+
+ const removeFileFromGroupList = useCallback(
+ (groupList: GroupListItem, filePath: string): GroupListItem => {
+ const newGroupList: GroupListItem = {
+ tags: { ...groupList.tags },
+ folders: { ...groupList.folders },
+ extensions: { ...groupList.extensions },
+ notes: [...groupList.notes],
+ };
+
+ const removeFileFromGroupObject = (groupObject: Record>) => {
+ Object.keys(groupObject).forEach((key) => {
+ groupObject[key] = groupObject[key].filter((item) => item.id !== filePath);
+ });
+ };
+
+ removeFileFromGroupObject(newGroupList.tags);
+ removeFileFromGroupObject(newGroupList.folders);
+ removeFileFromGroupObject(newGroupList.extensions);
+
+ // Remove file from notes
+ newGroupList.notes = newGroupList.notes.filter((item) => item.id !== filePath);
+
+ return newGroupList;
+ },
+ []
+ );
+
+ const setActiveState = useCallback(
+ (section: ActiveSection, item: ActiveItem = null, clearSearch: boolean = true) => {
+ setActiveSection(section);
+ setActiveItem(item);
+ if (clearSearch) {
+ setSearchTerm("");
+ }
+ },
+ []
+ );
+
+ // Unified processor
+ const groupHandlers = useMemo(() => {
+ const createDeleteHandler = (groupType: keyof Omit) => {
+ return (e: React.MouseEvent, item: SectionItem) => {
+ e.stopPropagation();
+
+ setGroupList((prev) => {
+ const newGroup = { ...prev[groupType] };
+ delete newGroup[item.id];
+ return {
+ ...prev,
+ [groupType]: newGroup,
+ };
+ });
+ };
+ };
+
+ return {
+ delete: {
+ tag: createDeleteHandler("tags"),
+ folder: createDeleteHandler("folders"),
+ extension: createDeleteHandler("extensions"),
+ },
+
+ add: {
+ tag: () => {
+ new TagSearchModal(app, (tagName) => {
+ const tagPattern = getTagPattern(tagName);
+ addPatternToGroup("tags", tagPattern, { tagPatterns: [tagPattern] });
+ }).open();
+ },
+
+ folder: () => {
+ new FolderSearchModal(app, (folderPath) => {
+ addPatternToGroup("folders", folderPath, { folderPatterns: [folderPath] });
+ }).open();
+ },
+
+ file: () => {
+ new ProjectFileSelectModal({
+ app,
+ onFileSelect: (file: TFile) => {
+ const existingNote = groupList.notes.find((note) => note.id === file.path);
+ if (existingNote) return;
+
+ // remove file from ignore
+ setIgnoreItems((prev) => {
+ const newFiles = new Set(prev.files);
+ newFiles.delete(file);
+ return { ...prev, files: newFiles };
+ });
+
+ setGroupList((prev) => ({
+ ...prev,
+ notes: [...prev.notes, { id: file.path, name: file.basename }],
+ }));
+ },
+ excludeFilePaths: [],
+ titleOnly: false,
+ }).open();
+ },
+
+ extension: () => {
+ // todo(emt-lin):maybe use this in the future
+ new Notice("Adding extension is temporarily not supported.");
+ return;
+ /*new ExtensionInputModal(app, (extension: string) => {
+ if (extension.trim() === "") return;
+ const extensionPattern = getExtensionPattern(extension);
+ addPatternToGroup("extensions", extensionPattern, {
+ extensionPatterns: [extensionPattern],
+ });
+ }).open();*/
+ },
+
+ ignoreFile: () => {
+ new ProjectFileSelectModal({
+ app,
+ onFileSelect: (file: TFile) => {
+ const isAlreadyIgnored = ignoreItems.files.has(file);
+ if (isAlreadyIgnored) return;
+
+ setIgnoreItems((prev) => {
+ const newFiles = new Set(prev.files);
+ newFiles.add(file);
+ return { ...prev, files: newFiles };
+ });
+
+ // Remove related files from the groupList
+ setGroupList((prev) => removeFileFromGroupList(prev, file.path));
+ },
+ excludeFilePaths: [],
+ titleOnly: false,
+ }).open();
+ },
+ },
+
+ click: {
+ tag: (tagId: string) => {
+ setActiveState("tags", tagId);
+ },
+
+ folder: (folderId: string) => {
+ setActiveState("folders", folderId);
+ },
+
+ extension: (extension: string) => {
+ setActiveState("extensions", extension);
+ },
+
+ files: () => {
+ setActiveState("files");
+ },
+
+ ignoreFiles: () => {
+ setActiveState("ignoreFiles");
+ },
+ },
+ };
+ }, [
+ app,
+ groupList.notes,
+ ignoreItems.files,
+ addPatternToGroup,
+ removeFileFromGroupList,
+ setActiveState,
+ ]);
+
+ const getDisplayTitle = () => {
+ if (searchTerm) return `Search Results for: "${searchTerm}"`;
+ if (activeSection === "tags" && activeItem) {
+ return `Tag: ${activeItem}`;
+ }
+ if (activeSection === "folders" && activeItem) {
+ return `Folder: ${activeItem}`;
+ }
+ if (activeSection === "files") return "Files";
+ if (activeSection === "extensions" && activeItem) {
+ return `Extension: ${activeItem}`;
+ }
+ if (activeSection === "ignoreFiles") return "Ignore Files";
+ return "Select a category to view items";
+ };
+
+ const handleDeleteItem = (e: React.MouseEvent, item: GroupItem) => {
+ e.stopPropagation();
+
+ const file = appAllFiles.find((file) => file.path === item.id);
+ if (file) {
+ // add file to ignore
+ setIgnoreItems((prev) => {
+ const newFiles = new Set(prev.files);
+ newFiles.add(file);
+ return { ...prev, files: newFiles };
+ });
+
+ setGroupList((prev) => removeFileFromGroupList(prev, item.id));
+ }
+ };
+
+ const refreshGroupListFromCurrentPatterns = useCallback(() => {
+ const currentInclude = convertGroupListToInclusions(latestGroupList.current, appAllFiles);
+ const currentExclude = convertDeletedItemsToExclusions(latestIgnoreItems.current);
+
+ const { inclusions, exclusions } = getMatchingPatterns({
+ inclusions: currentInclude,
+ exclusions: currentExclude,
+ isProject: true,
+ });
+
+ const newGroupList = createAndPopulateGroupList(appAllFiles, inclusions, exclusions);
+ setGroupList(newGroupList);
+ }, [
+ appAllFiles,
+ convertDeletedItemsToExclusions,
+ convertGroupListToInclusions,
+ createAndPopulateGroupList,
+ ]);
+
+ const handleDeleteIgnoreItem = (e: React.MouseEvent, item: GroupItem) => {
+ e.stopPropagation();
+
+ const file = appAllFiles.find((file) => file.path === item.id);
+
+ if (file) {
+ setIgnoreItems((prev) => {
+ const newFiles = new Set(prev.files);
+ newFiles.delete(file);
+ return { ...prev, files: newFiles };
+ });
+
+ // refresh groupList
+ refreshGroupListFromCurrentPatterns();
+ }
+ };
+
+ const handleSave = () => {
+ const include = convertGroupListToInclusions(groupList, appAllFiles);
+ const exclude = convertDeletedItemsToExclusions(ignoreItems);
+ onSave({
+ ...initialProject,
+ contextSource: {
+ ...initialProject.contextSource,
+ inclusions: include,
+ exclusions: exclude,
+ },
+ });
+ };
+
+ return (
+
+
+ {/* Left Sidebar - Navigation */}
+
+
+ {/* Header */}
+
+
Project Context
+
+
+
+
+ {/* Tags Section */}
+
name.slice(1))}
+ activeItem={activeItem}
+ activeSection={activeSection}
+ sectionType="tags"
+ renderIcon={(_item) => # }
+ onItemClick={groupHandlers.click.tag}
+ onAddClick={groupHandlers.add.tag}
+ onDeleteItem={(e, item) => groupHandlers.delete.tag(e, item)}
+ />
+
+
+
+ {/* Folders Section */}
+ groupHandlers.delete.folder(e, item)}
+ />
+
+
+
+ {/* Files Section */}
+
+
+
+ Files ({groupList.notes.length})
+
+
+
+
+
+ {/* todo(emt-lin):maybe use this in the future */}
+ {/* Extensions Section */}
+ {/* }
+ onItemClick={groupHandlers.click.extension}
+ onAddClick={groupHandlers.add.extension}
+ onDeleteItem={(e, item) => groupHandlers.delete.extension(e, item)}
+ />
+
+ */}
+
+ {/* Ignore Files Section */}
+
+
+
+ Ignore Files ({ignoreItems.files.size})
+
+
+
+
+
+
+
+
+
+ {/* Right Content Area */}
+
+
+ {/* Header */}
+
+ {
+ setSearchTerm(v);
+ if (v) {
+ setActiveState("search", null, false);
+ }
+ }}
+ />
+
+
+ {/* Content Title */}
+
+
{getDisplayTitle()}
+
+
+ {/* Content Area */}
+
+ {getDisplayItems.length === 0 ? (
+
+ {activeSection
+ ? "No items found."
+ : "Select a category from the sidebar to view items."}
+
+ ) : (
+
+ {sortItems(getDisplayItems).map((item) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ Cancel
+
+ Save
+
+
+ );
+}
+
+export class ContextManageModal extends Modal {
+ private root: Root;
+
+ constructor(
+ app: App,
+ private onSave: (project: ProjectConfig) => void,
+ private initialProject: ProjectConfig
+ ) {
+ super(app);
+ }
+
+ onOpen() {
+ const { contentEl, modalEl } = this;
+ this.root = createRoot(contentEl);
+
+ modalEl.style.minWidth = "50vw";
+
+ const handleSave = (project: ProjectConfig) => {
+ this.onSave(project);
+ this.close();
+ };
+
+ const handleCancel = () => {
+ this.close();
+ };
+
+ this.root.render(
+
+ );
+ }
+
+ onClose() {
+ if (this.root) {
+ this.root.unmount();
+ }
+ }
+}
diff --git a/src/components/ui/SearchBar.tsx b/src/components/ui/SearchBar.tsx
new file mode 100644
index 00000000..c504cfe2
--- /dev/null
+++ b/src/components/ui/SearchBar.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { XCircle, Search } from "lucide-react";
+
+interface SearchBarProps {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+}
+
+export const SearchBar: React.FC = ({
+ value,
+ onChange,
+ placeholder = "Search...",
+}) => {
+ return (
+
+ onChange(e.target.value)}
+ className="tw-pr-10" // Add padding to prevent text overlap with icons
+ />
+ {value && (
+ onChange("")}
+ className="tw-absolute tw-right-8 tw-top-1/2 tw-size-4 -tw-translate-y-1/2 tw-transform tw-rounded-full tw-p-0 tw-transition-colors"
+ aria-label="Clear search"
+ >
+
+
+ )}
+
+
+ );
+};
diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx
new file mode 100644
index 00000000..ba4e7543
--- /dev/null
+++ b/src/components/ui/resizable.tsx
@@ -0,0 +1,45 @@
+import * as React from "react";
+import { GripVertical } from "lucide-react";
+import * as ResizablePrimitive from "react-resizable-panels";
+
+import { cn } from "@/lib/utils";
+
+const ResizablePanelGroup = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+);
+
+const ResizablePanel = ResizablePrimitive.Panel;
+
+const ResizableHandle = ({
+ withHandle,
+ className,
+ ...props
+}: React.ComponentProps & {
+ withHandle?: boolean;
+}) => (
+ // tw-bg-border -> --divider-color: var(--background-modifier-border);
+ div]:tw-rotate-90",
+ className
+ )}
+ {...props}
+ >
+ {withHandle && (
+
+
+
+ )}
+
+);
+
+export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx
new file mode 100644
index 00000000..253a9f97
--- /dev/null
+++ b/src/components/ui/scroll-area.tsx
@@ -0,0 +1,48 @@
+import * as React from "react";
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
+
+import { cn } from "@/lib/utils";
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {/*[&>div:first-child]:!tw-block:for override the first child div style's (display: table)*/}
+
+ {children}
+
+
+
+
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+ {/* tw-bg-border -> --divider-color: var(--background-modifier-border); */}
+
+
+));
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
+
+export { ScrollArea, ScrollBar };
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
new file mode 100644
index 00000000..14a4a35b
--- /dev/null
+++ b/src/components/ui/separator.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import * as React from "react";
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+
+import { cn } from "@/lib/utils";
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
+ // tw-bg-border -> --divider-color: var(--background-modifier-border);
+
+));
+Separator.displayName = SeparatorPrimitive.Root.displayName;
+
+export { Separator };
diff --git a/src/search/searchUtils.ts b/src/search/searchUtils.ts
index c4275b33..4111fc73 100644
--- a/src/search/searchUtils.ts
+++ b/src/search/searchUtils.ts
@@ -5,7 +5,7 @@ import { getTagsFromNote, stripHash } from "@/utils";
import { Embeddings } from "@langchain/core/embeddings";
import { App, TFile } from "obsidian";
-interface PatternCategory {
+export interface PatternCategory {
tagPatterns?: string[];
extensionPatterns?: string[];
folderPatterns?: string[];
@@ -331,3 +331,15 @@ export function extractAppIgnoreSettings(app: App): string[] {
return appIgnoreFolders;
}
+
+export function getTagPattern(tag: string): string {
+ return `#${tag}`;
+}
+
+export function getFilePattern(file: TFile): string {
+ return `[[${file.basename}]]`;
+}
+
+export function getExtensionPattern(extension: string): string {
+ return `*.${extension}`;
+}
diff --git a/src/tests/context-manage-modal.test.ts b/src/tests/context-manage-modal.test.ts
new file mode 100644
index 00000000..359cd0e5
--- /dev/null
+++ b/src/tests/context-manage-modal.test.ts
@@ -0,0 +1,546 @@
+import { TFile } from "obsidian";
+import {
+ createPatternSettingsValue,
+ getFilePattern,
+ shouldIndexFile,
+ PatternCategory,
+} from "@/search/searchUtils";
+
+// Mock dependencies
+jest.mock("obsidian", () => ({
+ TFile: class TFile {
+ path: string;
+ basename: string;
+ extension: string;
+
+ constructor(path: string = "") {
+ this.path = path;
+ const parts = path.split("/");
+ const filename = parts[parts.length - 1];
+ this.basename = filename.replace(/\.[^/.]+$/, "");
+ this.extension = filename.split(".").pop() || "";
+ }
+ },
+}));
+
+jest.mock("@/search/searchUtils", () => ({
+ getMatchingPatterns: jest.fn(),
+ shouldIndexFile: jest.fn(),
+ getFilePattern: jest.fn(),
+ createPatternSettingsValue: jest.fn(),
+ categorizePatterns: jest.fn(),
+ getDecodedPatterns: jest.fn(),
+}));
+
+// Mock types and interfaces
+interface GroupItem {
+ id: string;
+ name: string;
+ isIgnored?: boolean;
+}
+
+interface GroupListItem {
+ tags: Record>;
+ folders: Record>;
+ extensions: Record>;
+ notes: Array;
+}
+
+interface IgnoreItems {
+ files: Set;
+}
+
+// Test helper functions
+const createTestFile = (path: string): TFile => {
+ const file = new TFile();
+ file.path = path;
+ const parts = path.split("/");
+ const filename = parts[parts.length - 1];
+ file.basename = filename.replace(/\.[^/.]+$/, "");
+ file.extension = filename.split(".").pop() || "";
+ return file;
+};
+
+const createMockPatternCategory = (overrides: Partial = {}): PatternCategory => ({
+ tagPatterns: [],
+ folderPatterns: [],
+ extensionPatterns: [],
+ notePatterns: [],
+ ...overrides,
+});
+
+// Implementation of the functions to test
+const createAndPopulateGroupList = (
+ appFiles: TFile[],
+ inclusionPatterns: PatternCategory | null,
+ exclusionPatterns: PatternCategory | null
+): GroupListItem => {
+ const projectAllFiles = appFiles.filter((file) =>
+ shouldIndexFile(file, inclusionPatterns, exclusionPatterns)
+ );
+
+ // Initialize groups
+ const tags: Record> = {};
+ const folders: Record> = {};
+ const extensions: Record> = {};
+ const notes: Array = [];
+
+ (inclusionPatterns?.tagPatterns ?? []).forEach((tag) => {
+ tags[tag] = [];
+ });
+ (inclusionPatterns?.folderPatterns ?? []).forEach((folder) => {
+ folders[folder] = [];
+ });
+ (inclusionPatterns?.extensionPatterns ?? []).forEach((extension) => {
+ extensions[extension] = [];
+ });
+
+ // Populate with matching files
+ projectAllFiles.forEach((file) => {
+ const groupItem: GroupItem = {
+ id: file.path,
+ name: file.basename,
+ };
+
+ // Add to notes array
+ notes.push(groupItem);
+
+ // Add to appropriate groups based on patterns
+ (inclusionPatterns?.tagPatterns ?? []).forEach((tag) => {
+ if (tags[tag]) {
+ tags[tag].push(groupItem);
+ }
+ });
+
+ (inclusionPatterns?.folderPatterns ?? []).forEach((folder) => {
+ if (folders[folder] && file.path.includes(folder)) {
+ folders[folder].push(groupItem);
+ }
+ });
+
+ (inclusionPatterns?.extensionPatterns ?? []).forEach((extension) => {
+ if (extensions[extension] && file.extension === extension.replace("*.", "")) {
+ extensions[extension].push(groupItem);
+ }
+ });
+ });
+
+ return { tags, folders, extensions, notes };
+};
+
+const convertGroupListToInclusions = (list: GroupListItem, appFiles: TFile[]): string => {
+ const tagPatterns = Object.keys(list.tags);
+ const folderPatterns = Object.keys(list.folders);
+ const extensionPatterns = Object.keys(list.extensions);
+ const notePatterns = list.notes
+ .map((note) => {
+ const file = appFiles.find((file) => file.path === note.id);
+ if (file) {
+ return getFilePattern(file);
+ }
+ })
+ .filter(Boolean) as string[];
+
+ return createPatternSettingsValue({
+ tagPatterns,
+ folderPatterns,
+ extensionPatterns,
+ notePatterns,
+ });
+};
+
+const convertDeletedItemsToExclusions = (items: IgnoreItems): string => {
+ const notePatterns = Array.from(items.files).map((file) => getFilePattern(file));
+ return createPatternSettingsValue({ notePatterns }) || "";
+};
+
+describe("Context Manage Modal Functions", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ // Setup default mocks
+ (shouldIndexFile as jest.Mock).mockReturnValue(true);
+ (getFilePattern as jest.Mock).mockImplementation((file: TFile) => `[[${file.basename}]]`);
+ (createPatternSettingsValue as jest.Mock).mockImplementation(
+ ({ tagPatterns = [], folderPatterns = [], extensionPatterns = [], notePatterns = [] }) => {
+ const patterns = [...tagPatterns, ...folderPatterns, ...extensionPatterns, ...notePatterns];
+ return patterns.join(",");
+ }
+ );
+ });
+
+ describe("createAndPopulateGroupList", () => {
+ it("Should create empty GroupList when there are no files", () => {
+ const result = createAndPopulateGroupList([], null, null);
+
+ expect(result).toEqual({
+ tags: {},
+ folders: {},
+ extensions: {},
+ notes: [],
+ });
+ });
+
+ it("Should correctly filter files based on inclusion and exclusion patterns", () => {
+ const files = [
+ createTestFile("test1.md"),
+ createTestFile("test2.pdf"),
+ createTestFile("test3.txt"),
+ ];
+
+ (shouldIndexFile as jest.Mock)
+ .mockReturnValueOnce(true)
+ .mockReturnValueOnce(false)
+ .mockReturnValueOnce(true);
+
+ const inclusionPatterns = createMockPatternCategory();
+ const exclusionPatterns = createMockPatternCategory();
+
+ const result = createAndPopulateGroupList(files, inclusionPatterns, exclusionPatterns);
+
+ expect(shouldIndexFile).toHaveBeenCalledTimes(3);
+ expect(result.notes).toHaveLength(2);
+ expect(result.notes[0].id).toBe("test1.md");
+ expect(result.notes[1].id).toBe("test3.txt");
+ });
+
+ it("Should initialize groups based on inclusion patterns", () => {
+ const files = [createTestFile("test.md")];
+ const inclusionPatterns = createMockPatternCategory({
+ tagPatterns: ["#important", "#work"],
+ folderPatterns: ["docs", "projects"],
+ extensionPatterns: ["*.md", "*.pdf"],
+ });
+
+ const result = createAndPopulateGroupList(files, inclusionPatterns, null);
+
+ expect(Object.keys(result.tags)).toEqual(["#important", "#work"]);
+ expect(Object.keys(result.folders)).toEqual(["docs", "projects"]);
+ expect(Object.keys(result.extensions)).toEqual(["*.md", "*.pdf"]);
+ });
+
+ it("Should add files to appropriate groups", () => {
+ const files = [createTestFile("docs/test.md"), createTestFile("projects/work.pdf")];
+
+ const inclusionPatterns = createMockPatternCategory({
+ folderPatterns: ["docs", "projects"],
+ extensionPatterns: ["*.md", "*.pdf"],
+ });
+
+ const result = createAndPopulateGroupList(files, inclusionPatterns, null);
+
+ expect(result.folders["docs"]).toHaveLength(1);
+ expect(result.folders["docs"][0].name).toBe("test");
+ expect(result.folders["projects"]).toHaveLength(1);
+ expect(result.folders["projects"][0].name).toBe("work");
+ });
+
+ it("Should handle duplicate patterns", () => {
+ const files = [createTestFile("test.md")];
+ const inclusionPatterns = createMockPatternCategory({
+ tagPatterns: ["#tag", "#tag", "#tag"],
+ folderPatterns: ["folder", "folder"],
+ });
+
+ const result = createAndPopulateGroupList(files, inclusionPatterns, null);
+
+ expect(Object.keys(result.tags)).toEqual(["#tag"]);
+ expect(Object.keys(result.folders)).toEqual(["folder"]);
+ });
+ });
+
+ describe("convertGroupListToInclusions", () => {
+ it("Should convert empty GroupList to empty string", () => {
+ const emptyGroupList: GroupListItem = {
+ tags: {},
+ folders: {},
+ extensions: {},
+ notes: [],
+ };
+
+ const result = convertGroupListToInclusions(emptyGroupList, []);
+
+ expect(createPatternSettingsValue).toHaveBeenCalledWith({
+ tagPatterns: [],
+ folderPatterns: [],
+ extensionPatterns: [],
+ notePatterns: [],
+ });
+ expect(result).toBe("");
+ });
+
+ it("Should correctly extract all pattern types", () => {
+ const groupList: GroupListItem = {
+ tags: { "#important": [], "#work": [] },
+ folders: { docs: [], projects: [] },
+ extensions: { "*.md": [], "*.pdf": [] },
+ notes: [
+ { id: "note1.md", name: "note1" },
+ { id: "note2.md", name: "note2" },
+ ],
+ };
+
+ const appFiles = [createTestFile("note1.md"), createTestFile("note2.md")];
+
+ convertGroupListToInclusions(groupList, appFiles);
+
+ expect(createPatternSettingsValue).toHaveBeenCalledWith({
+ tagPatterns: ["#important", "#work"],
+ folderPatterns: ["docs", "projects"],
+ extensionPatterns: ["*.md", "*.pdf"],
+ notePatterns: ["[[note1]]", "[[note2]]"],
+ });
+ });
+
+ it("Should handle notes without corresponding files", () => {
+ const groupList: GroupListItem = {
+ tags: {},
+ folders: {},
+ extensions: {},
+ notes: [
+ { id: "missing.md", name: "missing" },
+ { id: "existing.md", name: "existing" },
+ ],
+ };
+
+ const appFiles = [createTestFile("existing.md")];
+
+ convertGroupListToInclusions(groupList, appFiles);
+
+ expect(createPatternSettingsValue).toHaveBeenCalledWith({
+ tagPatterns: [],
+ folderPatterns: [],
+ extensionPatterns: [],
+ notePatterns: ["[[existing]]"],
+ });
+ });
+ });
+
+ describe("convertDeletedItemsToExclusions", () => {
+ it("Should convert empty IgnoreItems to empty string", () => {
+ const emptyIgnoreItems: IgnoreItems = {
+ files: new Set(),
+ };
+
+ const result = convertDeletedItemsToExclusions(emptyIgnoreItems);
+
+ expect(createPatternSettingsValue).toHaveBeenCalledWith({
+ notePatterns: [],
+ });
+ expect(result).toBe("");
+ });
+
+ it("Should correctly convert ignored files to exclusion patterns", () => {
+ const file1 = createTestFile("ignore1.md");
+ const file2 = createTestFile("ignore2.pdf");
+
+ const ignoreItems: IgnoreItems = {
+ files: new Set([file1, file2]),
+ };
+
+ convertDeletedItemsToExclusions(ignoreItems);
+
+ expect(getFilePattern).toHaveBeenCalledWith(file1);
+ expect(getFilePattern).toHaveBeenCalledWith(file2);
+ expect(createPatternSettingsValue).toHaveBeenCalledWith({
+ notePatterns: ["[[ignore1]]", "[[ignore2]]"],
+ });
+ });
+
+ it("Should handle a single ignored file", () => {
+ const file = createTestFile("single.md");
+ const ignoreItems: IgnoreItems = {
+ files: new Set([file]),
+ };
+
+ convertDeletedItemsToExclusions(ignoreItems);
+
+ expect(getFilePattern).toHaveBeenCalledWith(file);
+ expect(createPatternSettingsValue).toHaveBeenCalledWith({
+ notePatterns: ["[[single]]"],
+ });
+ });
+
+ it("Should handle case when createPatternSettingsValue returns undefined", () => {
+ (createPatternSettingsValue as jest.Mock).mockReturnValueOnce(undefined);
+
+ const file = createTestFile("test.md");
+ const ignoreItems: IgnoreItems = {
+ files: new Set([file]),
+ };
+
+ const result = convertDeletedItemsToExclusions(ignoreItems);
+
+ expect(result).toBe("");
+ });
+ });
+
+ // Boundary conditions and error handling tests
+ describe("Boundary Condition Tests", () => {
+ describe("createAndPopulateGroupList", () => {
+ it("Should handle null inclusion patterns", () => {
+ const files = [createTestFile("test.md")];
+ const result = createAndPopulateGroupList(files, null, null);
+
+ expect(result.tags).toEqual({});
+ expect(result.folders).toEqual({});
+ expect(result.extensions).toEqual({});
+ expect(result.notes).toHaveLength(1);
+ });
+
+ it("Should handle file paths with special characters", () => {
+ const files = [
+ createTestFile("folder with spaces/file-with-dashes.md"),
+ createTestFile("中文文件夹/中文文件.md"),
+ createTestFile("folder/file@#$%.md"),
+ ];
+
+ const result = createAndPopulateGroupList(files, null, null);
+
+ expect(result.notes).toHaveLength(3);
+ expect(result.notes[0].name).toBe("file-with-dashes");
+ expect(result.notes[1].name).toBe("中文文件");
+ expect(result.notes[2].name).toBe("file@#$%");
+ });
+ });
+
+ describe("convertGroupListToInclusions", () => {
+ it("Should handle large number of patterns", () => {
+ const tags: Record> = {};
+ const folders: Record> = {};
+ const extensions: Record> = {};
+
+ // Create many patterns
+ for (let i = 0; i < 100; i++) {
+ tags[`#tag${i}`] = [];
+ folders[`folder${i}`] = [];
+ extensions[`*.ext${i}`] = [];
+ }
+
+ const groupList: GroupListItem = { tags, folders, extensions, notes: [] };
+ convertGroupListToInclusions(groupList, []);
+
+ expect(createPatternSettingsValue).toHaveBeenCalledWith({
+ tagPatterns: expect.arrayContaining([`#tag0`, `#tag99`]),
+ folderPatterns: expect.arrayContaining([`folder0`, `folder99`]),
+ extensionPatterns: expect.arrayContaining([`*.ext0`, `*.ext99`]),
+ notePatterns: [],
+ });
+ });
+
+ it("Should handle invalid file paths in notes", () => {
+ const groupList: GroupListItem = {
+ tags: {},
+ folders: {},
+ extensions: {},
+ notes: [
+ { id: "", name: "" },
+ { id: " ", name: " " },
+ { id: "valid.md", name: "valid" },
+ ],
+ };
+
+ const appFiles = [createTestFile("valid.md")];
+ convertGroupListToInclusions(groupList, appFiles);
+
+ expect(createPatternSettingsValue).toHaveBeenCalledWith({
+ tagPatterns: [],
+ folderPatterns: [],
+ extensionPatterns: [],
+ notePatterns: ["[[valid]]"],
+ });
+ });
+ });
+
+ describe("convertDeletedItemsToExclusions", () => {
+ it("Should handle large number of ignored files", () => {
+ const files = new Set();
+ for (let i = 0; i < 1000; i++) {
+ files.add(createTestFile(`file${i}.md`));
+ }
+
+ const ignoreItems: IgnoreItems = { files };
+ convertDeletedItemsToExclusions(ignoreItems);
+
+ expect(getFilePattern).toHaveBeenCalledTimes(1000);
+ expect(createPatternSettingsValue).toHaveBeenCalledWith({
+ notePatterns: expect.arrayContaining(["[[file0]]", "[[file999]]"]),
+ });
+ });
+
+ it("Should handle filenames with special characters", () => {
+ const files = [
+ createTestFile("file with spaces.md"),
+ createTestFile("file-with-dashes.md"),
+ createTestFile("file_with_underscores.md"),
+ createTestFile("file@#$%.md"),
+ ];
+
+ const ignoreItems: IgnoreItems = { files: new Set(files) };
+ convertDeletedItemsToExclusions(ignoreItems);
+
+ expect(getFilePattern).toHaveBeenCalledTimes(4);
+ files.forEach((file) => {
+ expect(getFilePattern).toHaveBeenCalledWith(file);
+ });
+ });
+ });
+ });
+
+ // Integration tests
+ describe("Integration Tests", () => {
+ it("Should correctly handle the complete workflow", () => {
+ // 1. Create initial file list
+ const appFiles = [
+ createTestFile("docs/readme.md"),
+ createTestFile("src/main.ts"),
+ createTestFile("tests/test.spec.ts"),
+ createTestFile("config.json"),
+ ];
+
+ // 2. Set inclusion patterns
+ const inclusionPatterns = createMockPatternCategory({
+ folderPatterns: ["docs", "src"],
+ extensionPatterns: ["*.md", "*.ts"],
+ });
+
+ // 3. Create GroupList
+ const groupList = createAndPopulateGroupList(appFiles, inclusionPatterns, null);
+
+ // 4. Convert to inclusions
+ const inclusions = convertGroupListToInclusions(groupList, appFiles);
+
+ // 5. Create ignore items and convert to exclusions
+ const ignoreItems: IgnoreItems = {
+ files: new Set([createTestFile("config.json")]),
+ };
+ const exclusions = convertDeletedItemsToExclusions(ignoreItems);
+
+ // Verify results
+ expect(groupList.folders).toHaveProperty("docs");
+ expect(groupList.folders).toHaveProperty("src");
+ expect(Object.prototype.hasOwnProperty.call(groupList.extensions, "*.md")).toBe(true);
+ expect(Object.prototype.hasOwnProperty.call(groupList.extensions, "*.ts")).toBe(true);
+
+ expect(createPatternSettingsValue).toHaveBeenCalledWith({
+ tagPatterns: [],
+ folderPatterns: ["docs", "src"],
+ extensionPatterns: ["*.md", "*.ts"],
+ notePatterns: expect.any(Array),
+ });
+
+ expect(createPatternSettingsValue).toHaveBeenCalledWith({
+ notePatterns: ["[[config]]"],
+ });
+
+ // Verify inclusions and exclusions variables
+ // Since createPatternSettingsValue is mocked to return all patterns as comma-separated list
+ expect(inclusions).toContain("docs");
+ expect(inclusions).toContain("src");
+ expect(inclusions).toContain("*.md");
+ expect(inclusions).toContain("*.ts");
+
+ expect(exclusions).toBe("[[config]]");
+ });
+ });
+});
diff --git a/tailwind.config.js b/tailwind.config.js
index 8d8b7edd..e58ec7e7 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -40,6 +40,14 @@ module.exports = {
green: "var(--color-green)",
blue: "var(--color-blue)",
},
+ "context-manager": {
+ blue: "var(--color-blue)",
+ green: "var(--color-green)",
+ red: "var(--color-red)",
+ yellow: "var(--color-yellow)",
+ orange: "var(--color-orange)",
+ purple: "var(--color-purple)",
+ },
},
backgroundColor: {
inherit: colors.inherit,