feat(web): resizable panels via svelte-splitpanes + Claude-style composer
Replace hand-rolled pointer-resize logic (TaskContextPanel's 3-way vertical split, SessionChatWindow's rail, ChatThread's message/input split) with svelte-splitpanes, themed onto the app's existing border/primary tokens. - TaskContextPanel: Scope/Plan/Event-log sections collapse to a fixed header height and restore their last size on reopen. - ChatThread: input area is now a separate resizable pane, clamped to a measured one-line minimum and a 45% max, instead of a fixed max-h textarea. - Send button restyled to sit inside the input's corner (Claude-style), swapping the up-arrow for a corner-down-left return icon. - Adds a $app/environment shim + optimizeDeps exclude, since svelte-splitpanes assumes SvelteKit and this is a plain Vite app. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
31
web/package-lock.json
generated
31
web/package-lock.json
generated
@@ -13,6 +13,7 @@
|
|||||||
"d3-force": "^3.0.0",
|
"d3-force": "^3.0.0",
|
||||||
"dompurify": "^3.4.11",
|
"dompurify": "^3.4.11",
|
||||||
"marked": "^18.0.5",
|
"marked": "^18.0.5",
|
||||||
|
"svelte-splitpanes": "^8.0.12",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"uplot": "^1.6.32"
|
"uplot": "^1.6.32"
|
||||||
},
|
},
|
||||||
@@ -877,7 +878,6 @@
|
|||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
@@ -888,7 +888,6 @@
|
|||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
@@ -899,7 +898,6 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@@ -909,14 +907,12 @@
|
|||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.31",
|
"version": "0.3.31",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
@@ -1320,7 +1316,6 @@
|
|||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz",
|
||||||
"integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==",
|
"integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"acorn": "^8.9.0"
|
"acorn": "^8.9.0"
|
||||||
@@ -1677,7 +1672,6 @@
|
|||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
@@ -1691,7 +1685,6 @@
|
|||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
@@ -1844,7 +1837,7 @@
|
|||||||
"version": "8.64.0",
|
"version": "8.64.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
|
||||||
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -2066,7 +2059,6 @@
|
|||||||
"version": "8.17.0",
|
"version": "8.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||||
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
@@ -2139,7 +2131,6 @@
|
|||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||||
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -2166,7 +2157,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -2548,7 +2538,6 @@
|
|||||||
"version": "5.8.1",
|
"version": "5.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz",
|
||||||
"integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==",
|
"integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
@@ -2858,7 +2847,6 @@
|
|||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
@@ -2896,7 +2884,6 @@
|
|||||||
"version": "2.2.13",
|
"version": "2.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz",
|
||||||
"integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==",
|
"integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
@@ -3364,7 +3351,6 @@
|
|||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||||
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "^1.0.6"
|
"@types/estree": "^1.0.6"
|
||||||
@@ -3788,7 +3774,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
@@ -3842,7 +3827,6 @@
|
|||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
@@ -4504,7 +4488,6 @@
|
|||||||
"version": "5.56.4",
|
"version": "5.56.4",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz",
|
||||||
"integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==",
|
"integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.4",
|
"@jridgewell/remapping": "^2.3.4",
|
||||||
@@ -4659,6 +4642,15 @@
|
|||||||
"svelte": "^5.7.0"
|
"svelte": "^5.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svelte-splitpanes": {
|
||||||
|
"version": "8.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-splitpanes/-/svelte-splitpanes-8.0.12.tgz",
|
||||||
|
"integrity": "sha512-HJ07HgbtY0Q/35TEuJquGy47dtgCVavV7ay9r1FhWRx3boyUs3RpeiHlwMKliznCKy2ZotNeT+8GG+mIoNjgRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.43.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte-toolbelt": {
|
"node_modules/svelte-toolbelt": {
|
||||||
"version": "0.10.6",
|
"version": "0.10.6",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz",
|
||||||
@@ -6315,7 +6307,6 @@
|
|||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
||||||
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"d3-force": "^3.0.0",
|
"d3-force": "^3.0.0",
|
||||||
"dompurify": "^3.4.11",
|
"dompurify": "^3.4.11",
|
||||||
"marked": "^18.0.5",
|
"marked": "^18.0.5",
|
||||||
|
"svelte-splitpanes": "^8.0.12",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"uplot": "^1.6.32"
|
"uplot": "^1.6.32"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,3 +289,32 @@ a {
|
|||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* svelte-splitpanes theming (TaskContextPanel, SessionChatWindow rail) —
|
||||||
|
mapped onto the app's border/primary tokens instead of the library's
|
||||||
|
default-theme, so splitters follow the terracotta/dark theme toggle. */
|
||||||
|
.splitpanes.oikos-theme .splitpanes__pane {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splitpanes.oikos-theme .splitpanes__splitter {
|
||||||
|
background-color: transparent;
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splitpanes.oikos-theme .splitpanes__splitter:hover,
|
||||||
|
.splitpanes.oikos-theme .splitpanes__splitter.splitpanes__splitter__active {
|
||||||
|
background-color: color-mix(in oklab, var(--primary) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oikos-theme.splitpanes--horizontal > .splitpanes__splitter {
|
||||||
|
height: 6px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oikos-theme.splitpanes--vertical > .splitpanes__splitter {
|
||||||
|
width: 6px;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
// window (its own per-session store bundle from chat.ts's chatFor) render
|
// window (its own per-session store bundle from chat.ts's chatFor) render
|
||||||
// through this, so the message-bubble/markdown styling lives in one place
|
// through this, so the message-bubble/markdown styling lives in one place
|
||||||
// instead of being copy-pasted between the two.
|
// instead of being copy-pasted between the two.
|
||||||
|
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||||
import { activityLog } from '$lib/stores/activity'
|
import { activityLog } from '$lib/stores/activity'
|
||||||
import AgentIndicator from '$lib/components/AgentIndicator.svelte'
|
import AgentIndicator from '$lib/components/AgentIndicator.svelte'
|
||||||
import { Button } from '$lib/components/ui/button'
|
import { Button } from '$lib/components/ui/button'
|
||||||
import { Textarea } from '$lib/components/ui/textarea'
|
import { Textarea } from '$lib/components/ui/textarea'
|
||||||
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up'
|
import CornerDownLeftIcon from '@lucide/svelte/icons/corner-down-left'
|
||||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw'
|
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw'
|
||||||
import SquareIcon from '@lucide/svelte/icons/square'
|
import SquareIcon from '@lucide/svelte/icons/square'
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
@@ -44,6 +45,44 @@
|
|||||||
let scrolledUp = $state(false)
|
let scrolledUp = $state(false)
|
||||||
let container = $state<HTMLDivElement | null>(null)
|
let container = $state<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
// Resizable input area — drag the splitter above it to grow the textarea,
|
||||||
|
// capped so it can't swallow the whole thread. Both the minimum and the
|
||||||
|
// default are exactly one line: measured from the textarea's own
|
||||||
|
// line-height/padding/border rather than hardcoded, so it stays correct if
|
||||||
|
// that styling ever changes.
|
||||||
|
let threadHeight = $state(0)
|
||||||
|
let textareaRef = $state<HTMLTextAreaElement | null>(null)
|
||||||
|
let inputWrapperRef = $state<HTMLDivElement | null>(null)
|
||||||
|
let oneLinePx = $state(64)
|
||||||
|
$effect(() => {
|
||||||
|
if (!textareaRef || !inputWrapperRef) return
|
||||||
|
const taCs = getComputedStyle(textareaRef)
|
||||||
|
const lineHeight = parseFloat(taCs.lineHeight)
|
||||||
|
if (!Number.isFinite(lineHeight)) return
|
||||||
|
const taBoxY =
|
||||||
|
parseFloat(taCs.paddingTop) + parseFloat(taCs.paddingBottom) + parseFloat(taCs.borderTopWidth) + parseFloat(taCs.borderBottomWidth)
|
||||||
|
// The wrapper's own padding/border (space around the textarea, not part
|
||||||
|
// of it) also has to fit inside the minimum, or the textarea gets
|
||||||
|
// squeezed below one line once the pane is dragged down to it.
|
||||||
|
const wrapperCs = getComputedStyle(inputWrapperRef)
|
||||||
|
const wrapperBoxY =
|
||||||
|
parseFloat(wrapperCs.paddingTop) +
|
||||||
|
parseFloat(wrapperCs.paddingBottom) +
|
||||||
|
parseFloat(wrapperCs.borderTopWidth) +
|
||||||
|
parseFloat(wrapperCs.borderBottomWidth)
|
||||||
|
oneLinePx = lineHeight + taBoxY + wrapperBoxY
|
||||||
|
})
|
||||||
|
const inputMinSize = $derived(threadHeight > 0 ? (oneLinePx / threadHeight) * 100 : 12)
|
||||||
|
|
||||||
|
let inputSize = $state(12)
|
||||||
|
let inputSizeDefaulted = false
|
||||||
|
$effect(() => {
|
||||||
|
if (!inputSizeDefaulted && threadHeight > 0) {
|
||||||
|
inputSize = inputMinSize
|
||||||
|
inputSizeDefaulted = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function isNearBottom(): boolean {
|
function isNearBottom(): boolean {
|
||||||
if (!container) return true
|
if (!container) return true
|
||||||
const { scrollTop, scrollHeight, clientHeight } = container
|
const { scrollTop, scrollHeight, clientHeight } = container
|
||||||
@@ -87,7 +126,9 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex min-h-0 min-w-0 flex-1 flex-col">
|
<div class="flex h-full min-h-0 min-w-0 flex-col" bind:clientHeight={threadHeight}>
|
||||||
|
<Splitpanes horizontal theme="oikos-theme" dblClickSplitter={false} class="min-h-0 flex-1">
|
||||||
|
<Pane class="flex flex-col">
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
|
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
|
||||||
<div class="mx-auto flex max-w-3xl flex-col gap-5 p-4">
|
<div class="mx-auto flex max-w-3xl flex-col gap-5 p-4">
|
||||||
{#if messages.length === 0}
|
{#if messages.length === 0}
|
||||||
@@ -169,34 +210,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
</Pane>
|
||||||
|
|
||||||
<div class="border-t bg-card/50 p-3 input-ornament relative">
|
<Pane bind:size={inputSize} minSize={inputMinSize} maxSize={45} class="flex flex-col">
|
||||||
<form
|
<div class="flex h-full min-h-0 flex-col border-t bg-card/50 p-3 input-ornament relative" bind:this={inputWrapperRef}>
|
||||||
class="mx-auto flex max-w-3xl items-end gap-2"
|
<form
|
||||||
onsubmit={(e) => {
|
class="relative mx-auto flex h-full w-full max-w-3xl"
|
||||||
e.preventDefault()
|
onsubmit={(e) => {
|
||||||
submit()
|
e.preventDefault()
|
||||||
}}
|
submit()
|
||||||
>
|
}}
|
||||||
<Textarea
|
>
|
||||||
bind:value={input}
|
<Textarea
|
||||||
onkeydown={handleKeydown}
|
bind:ref={textareaRef}
|
||||||
placeholder="Ask Nomos anything…"
|
bind:value={input}
|
||||||
rows={2}
|
onkeydown={handleKeydown}
|
||||||
class="max-h-40 min-h-0 resize-none"
|
placeholder="Ask Nomos anything…"
|
||||||
disabled={streaming}
|
class="h-full max-h-none min-h-0 resize-none rounded-2xl px-4 py-3 pr-12 field-sizing-fixed"
|
||||||
/>
|
disabled={streaming}
|
||||||
{#if streaming}
|
/>
|
||||||
<Button type="button" size="icon" variant="destructive" onclick={onCancel} aria-label="Stop">
|
{#if streaming}
|
||||||
<SquareIcon />
|
<Button
|
||||||
</Button>
|
type="button"
|
||||||
{:else}
|
size="icon-sm"
|
||||||
<Button type="submit" size="icon" disabled={!input.trim()} aria-label="Send">
|
variant="secondary"
|
||||||
<ArrowUpIcon />
|
class="absolute right-2 bottom-2 rounded-lg"
|
||||||
</Button>
|
onclick={onCancel}
|
||||||
{/if}
|
aria-label="Stop"
|
||||||
</form>
|
>
|
||||||
</div>
|
<SquareIcon class="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="icon-sm"
|
||||||
|
variant="secondary"
|
||||||
|
class="absolute right-2 bottom-2 rounded-lg"
|
||||||
|
disabled={!input.trim()}
|
||||||
|
aria-label="Send"
|
||||||
|
>
|
||||||
|
<CornerDownLeftIcon class="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Pane>
|
||||||
|
</Splitpanes>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
// these can be open (and independently live) at once without the "which
|
// these can be open (and independently live) at once without the "which
|
||||||
// one's on screen" guarding the main page's singleton stores need.
|
// one's on screen" guarding the main page's singleton stores need.
|
||||||
import { onDestroy, onMount } from 'svelte'
|
import { onDestroy, onMount } from 'svelte'
|
||||||
|
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||||
import { chatFor, loadSessionChat, sendSessionMessage, cancelSessionStream, stopSessionPolling, dismissError, chatErrors } from '$lib/stores/chat'
|
import { chatFor, loadSessionChat, sendSessionMessage, cancelSessionStream, stopSessionPolling, dismissError, chatErrors } from '$lib/stores/chat'
|
||||||
import ChatThread from '$lib/components/ChatThread.svelte'
|
import ChatThread from '$lib/components/ChatThread.svelte'
|
||||||
import TaskContextPanel from '$lib/components/TaskContextPanel.svelte'
|
import TaskContextPanel from '$lib/components/TaskContextPanel.svelte'
|
||||||
@@ -30,29 +31,9 @@
|
|||||||
|
|
||||||
onDestroy(() => stopSessionPolling(sessionId))
|
onDestroy(() => stopSessionPolling(sessionId))
|
||||||
|
|
||||||
// Resizable right rail — same behavior as Chat.svelte's, sized smaller by
|
// Resizable right rail — sized smaller by default since task windows open
|
||||||
// default since task windows open narrower than the full page.
|
// narrower than the full page.
|
||||||
const RAIL_MIN = 220
|
let railSize = $state(24)
|
||||||
const RAIL_MAX = 480
|
|
||||||
let railWidth = $state(260)
|
|
||||||
let resizing = $state(false)
|
|
||||||
|
|
||||||
function startResize(e: PointerEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
resizing = true
|
|
||||||
const startX = e.clientX
|
|
||||||
const startW = railWidth
|
|
||||||
function move(ev: PointerEvent) {
|
|
||||||
railWidth = Math.min(RAIL_MAX, Math.max(RAIL_MIN, startW + (startX - ev.clientX)))
|
|
||||||
}
|
|
||||||
function up() {
|
|
||||||
resizing = false
|
|
||||||
window.removeEventListener('pointermove', move)
|
|
||||||
window.removeEventListener('pointerup', up)
|
|
||||||
}
|
|
||||||
window.addEventListener('pointermove', move)
|
|
||||||
window.addEventListener('pointerup', up)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full min-h-0">
|
<div class="flex h-full min-h-0">
|
||||||
@@ -64,34 +45,23 @@
|
|||||||
<p class="text-xs text-muted-foreground/70">It may have been deleted.</p>
|
<p class="text-xs text-muted-foreground/70">It may have been deleted.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<ChatThread
|
<Splitpanes theme="oikos-theme" dblClickSplitter={false}>
|
||||||
messages={$chatMessages}
|
<Pane>
|
||||||
streaming={$chatStreaming}
|
<ChatThread
|
||||||
connectionState={$chatConnectionState}
|
messages={$chatMessages}
|
||||||
error={$chatError}
|
streaming={$chatStreaming}
|
||||||
chatErrors={$chatErrors}
|
connectionState={$chatConnectionState}
|
||||||
onSend={(text) => sendSessionMessage(sessionId, text)}
|
error={$chatError}
|
||||||
onCancel={() => cancelSessionStream(sessionId)}
|
chatErrors={$chatErrors}
|
||||||
onReconnect={() => loadSessionChat(sessionId)}
|
onSend={(text) => sendSessionMessage(sessionId, text)}
|
||||||
onDismissError={dismissError}
|
onCancel={() => cancelSessionStream(sessionId)}
|
||||||
/>
|
onReconnect={() => loadSessionChat(sessionId)}
|
||||||
|
onDismissError={dismissError}
|
||||||
<div class="flex shrink-0" style="width: {railWidth}px">
|
/>
|
||||||
<button
|
</Pane>
|
||||||
type="button"
|
<Pane bind:size={railSize} minSize={18} maxSize={40}>
|
||||||
class="group/rz relative w-1.5 shrink-0 cursor-col-resize touch-none"
|
|
||||||
onpointerdown={startResize}
|
|
||||||
aria-label="Resize task panel"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 transition-colors {resizing
|
|
||||||
? 'bg-primary/60'
|
|
||||||
: 'bg-border group-hover/rz:bg-primary/50'}"
|
|
||||||
></span>
|
|
||||||
</button>
|
|
||||||
<div class="flex min-w-0 flex-1 flex-col">
|
|
||||||
<TaskContextPanel {sessionId} />
|
<TaskContextPanel {sessionId} />
|
||||||
</div>
|
</Pane>
|
||||||
</div>
|
</Splitpanes>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
|
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||||
import { startWorkspace, startSessionWorkspace, planSteps, currentTask, openQuestion, touched, healthDiffs, workspaceFor, taskFor } from '$lib/stores/workspace'
|
import { startWorkspace, startSessionWorkspace, planSteps, currentTask, openQuestion, touched, healthDiffs, workspaceFor, taskFor } from '$lib/stores/workspace'
|
||||||
import { streaming, messages, currentSession, chatFor } from '$lib/stores/chat'
|
import { streaming, messages, currentSession, chatFor } from '$lib/stores/chat'
|
||||||
import { activityLog, activityLogFor } from '$lib/stores/activity'
|
import { activityLog, activityLogFor } from '$lib/stores/activity'
|
||||||
@@ -44,39 +45,27 @@
|
|||||||
let planOpen = $state(true)
|
let planOpen = $state(true)
|
||||||
let activityOpen = $state(true)
|
let activityOpen = $state(true)
|
||||||
|
|
||||||
// Resize: distribute height across the 3 content areas.
|
// Resize: each section is a Pane in one vertical Splitpanes. Sizes are
|
||||||
// Heights stored in px, minus header sizes. Default even split.
|
// percentages of the panel's height; undefined means "share the space
|
||||||
let heights = $state([300, 200, 200])
|
// evenly with the other auto sections". Collapsing a section pins it to
|
||||||
let resizing = $state(-1)
|
// COLLAPSED_SIZE (roughly a header's worth of height) and remembers its
|
||||||
let resizeStartY = $state(0)
|
// last size so reopening restores it.
|
||||||
let resizeStartH = $state<[number, number]>([0, 0])
|
const COLLAPSED_SIZE = 6
|
||||||
|
const OPEN_MIN_SIZE = 12
|
||||||
|
let sizes = $state<(number | undefined)[]>([undefined, undefined, undefined])
|
||||||
|
// Reopening must restore a concrete number, never `undefined` — the pane
|
||||||
|
// only re-triggers the library's resize/equalize pass when `size` changes
|
||||||
|
// to a different *number*, so setting it back to `undefined` silently
|
||||||
|
// no-ops and leaves the section stuck at its collapsed height.
|
||||||
|
let savedSizes: number[] = [34, 33, 33]
|
||||||
|
|
||||||
function onPointerDown(i: number, e: PointerEvent) {
|
function toggleSection(i: number, isOpen: boolean) {
|
||||||
e.preventDefault()
|
if (isOpen) {
|
||||||
resizing = i
|
savedSizes[i] = sizes[i] ?? savedSizes[i]
|
||||||
resizeStartY = e.clientY
|
sizes[i] = COLLAPSED_SIZE
|
||||||
resizeStartH = [heights[i], heights[i + 1]]
|
} else {
|
||||||
window.addEventListener('pointermove', onPointerMove)
|
sizes[i] = savedSizes[i]
|
||||||
window.addEventListener('pointerup', onPointerUp)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function onPointerMove(e: PointerEvent) {
|
|
||||||
if (resizing < 0) return
|
|
||||||
const dy = e.clientY - resizeStartY
|
|
||||||
const minH = 80
|
|
||||||
// Clamp each section to minimum
|
|
||||||
const newA = Math.max(minH, resizeStartH[0] + dy)
|
|
||||||
const newB = Math.max(minH, resizeStartH[1] - dy)
|
|
||||||
const newHeights = [...heights]
|
|
||||||
newHeights[resizing] = newA
|
|
||||||
newHeights[resizing + 1] = newB
|
|
||||||
heights = newHeights
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPointerUp() {
|
|
||||||
resizing = -1
|
|
||||||
window.removeEventListener('pointermove', onPointerMove)
|
|
||||||
window.removeEventListener('pointerup', onPointerUp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plan collapsed status
|
// Plan collapsed status
|
||||||
@@ -103,54 +92,54 @@
|
|||||||
<div class="flex h-full min-h-0 flex-col">
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
<OperatorQuestion sessionId={effectiveSessionId} question={$openQuestionStore} />
|
<OperatorQuestion sessionId={effectiveSessionId} question={$openQuestionStore} />
|
||||||
|
|
||||||
<!-- Scope -->
|
<Splitpanes horizontal theme="oikos-theme" dblClickSplitter={false} class="min-h-0 flex-1">
|
||||||
<div class="flex shrink-0 flex-col border-b">
|
<!-- Scope -->
|
||||||
<button
|
<Pane bind:size={sizes[0]} minSize={scopeOpen ? OPEN_MIN_SIZE : COLLAPSED_SIZE} maxSize={scopeOpen ? 100 : COLLAPSED_SIZE} class="flex flex-col">
|
||||||
type="button"
|
<button
|
||||||
class="flex items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
type="button"
|
||||||
onclick={() => (scopeOpen = !scopeOpen)}
|
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
||||||
>
|
onclick={() => {
|
||||||
{#if scopeOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
toggleSection(0, scopeOpen)
|
||||||
<span>Scope</span>
|
scopeOpen = !scopeOpen
|
||||||
{#if !scopeOpen}
|
}}
|
||||||
<span class="ml-auto font-normal normal-case">{$touchedStore.length ? `${$touchedStore.length} entit${$touchedStore.length === 1 ? 'y' : 'ies'}` : 'Graph'}</span>
|
>
|
||||||
{/if}
|
{#if scopeOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
||||||
</button>
|
<span>Scope</span>
|
||||||
{#if scopeOpen}
|
{#if !scopeOpen}
|
||||||
<div style="height: {heights[0]}px">
|
<span class="ml-auto font-normal normal-case">{$touchedStore.length ? `${$touchedStore.length} entit${$touchedStore.length === 1 ? 'y' : 'ies'}` : 'Graph'}</span>
|
||||||
<SessionGraph messages={$messagesStore} touched={$touchedStore} healthDiffs={$healthDiffsStore} />
|
|
||||||
</div>
|
|
||||||
<!-- resize handle -->
|
|
||||||
<div
|
|
||||||
class="h-1.5 cursor-row-resize border-b hover:bg-primary/30 touch-none"
|
|
||||||
onpointerdown={(e) => onPointerDown(0, e)}
|
|
||||||
role="separator"
|
|
||||||
aria-orientation="horizontal"
|
|
||||||
></div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Plan -->
|
|
||||||
<div class="flex shrink-0 flex-col border-b">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
|
||||||
onclick={() => (planOpen = !planOpen)}
|
|
||||||
>
|
|
||||||
{#if planOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
|
||||||
<span>Plan</span>
|
|
||||||
{#if !planOpen}
|
|
||||||
{#if planTotal > 0}
|
|
||||||
<span class="ml-auto font-normal normal-case">Step {planDone}/{planTotal}</span>
|
|
||||||
{:else if $taskStore?.goal}
|
|
||||||
<span class="ml-auto max-w-[120px] truncate font-normal normal-case">{$taskStore.goal}</span>
|
|
||||||
{:else}
|
|
||||||
<span class="ml-auto font-normal normal-case text-muted-foreground">No plan yet</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#if scopeOpen}
|
||||||
|
<div class="min-h-0 flex-1">
|
||||||
|
<SessionGraph messages={$messagesStore} touched={$touchedStore} healthDiffs={$healthDiffsStore} />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</Pane>
|
||||||
{#if planOpen}
|
|
||||||
<div style="height: {heights[1]}px" class="flex flex-col overflow-y-auto">
|
<!-- Plan -->
|
||||||
|
<Pane bind:size={sizes[1]} minSize={planOpen ? OPEN_MIN_SIZE : COLLAPSED_SIZE} maxSize={planOpen ? 100 : COLLAPSED_SIZE} class="flex flex-col">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
||||||
|
onclick={() => {
|
||||||
|
toggleSection(1, planOpen)
|
||||||
|
planOpen = !planOpen
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if planOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
||||||
|
<span>Plan</span>
|
||||||
|
{#if !planOpen}
|
||||||
|
{#if planTotal > 0}
|
||||||
|
<span class="ml-auto font-normal normal-case">Step {planDone}/{planTotal}</span>
|
||||||
|
{:else if $taskStore?.goal}
|
||||||
|
<span class="ml-auto max-w-[120px] truncate font-normal normal-case">{$taskStore.goal}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="ml-auto font-normal normal-case text-muted-foreground">No plan yet</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#if planOpen}
|
||||||
|
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||||
{#if $taskStore?.goal}
|
{#if $taskStore?.goal}
|
||||||
<div class="flex items-start gap-2 px-3 py-2">
|
<div class="flex items-start gap-2 px-3 py-2">
|
||||||
<MilestoneIcon class="mt-0.5 size-3 shrink-0 text-primary" />
|
<MilestoneIcon class="mt-0.5 size-3 shrink-0 text-primary" />
|
||||||
@@ -244,38 +233,35 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<!-- resize handle -->
|
|
||||||
<div
|
|
||||||
class="h-1.5 cursor-row-resize border-b hover:bg-primary/30 touch-none"
|
|
||||||
onpointerdown={(e) => onPointerDown(1, e)}
|
|
||||||
role="separator"
|
|
||||||
aria-orientation="horizontal"
|
|
||||||
></div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Activity -->
|
|
||||||
<div class="flex min-h-0 flex-1 flex-col">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
|
||||||
onclick={() => (activityOpen = !activityOpen)}
|
|
||||||
>
|
|
||||||
{#if activityOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
|
||||||
<span>Event log</span>
|
|
||||||
{#if !activityOpen}
|
|
||||||
{#if $streamingStore && activityRunning > 0}
|
|
||||||
<Spinner class="size-3 text-primary" />
|
|
||||||
<span class="font-normal normal-case text-primary">{activityRunning} running</span>
|
|
||||||
{:else}
|
|
||||||
<span class="ml-auto font-normal normal-case">{activityCount || '—'} action{activityCount === 1 ? '' : 's'}</span>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</Pane>
|
||||||
{#if activityOpen}
|
|
||||||
<div class="min-h-0 flex-1 overflow-hidden">
|
<!-- Activity -->
|
||||||
<ActivityTimeline entries={$activityLogStore} />
|
<Pane bind:size={sizes[2]} minSize={activityOpen ? OPEN_MIN_SIZE : COLLAPSED_SIZE} maxSize={activityOpen ? 100 : COLLAPSED_SIZE} class="flex flex-col">
|
||||||
</div>
|
<button
|
||||||
{/if}
|
type="button"
|
||||||
</div>
|
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
||||||
|
onclick={() => {
|
||||||
|
toggleSection(2, activityOpen)
|
||||||
|
activityOpen = !activityOpen
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if activityOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
||||||
|
<span>Event log</span>
|
||||||
|
{#if !activityOpen}
|
||||||
|
{#if $streamingStore && activityRunning > 0}
|
||||||
|
<Spinner class="size-3 text-primary" />
|
||||||
|
<span class="font-normal normal-case text-primary">{activityRunning} running</span>
|
||||||
|
{:else}
|
||||||
|
<span class="ml-auto font-normal normal-case">{activityCount || '—'} action{activityCount === 1 ? '' : 's'}</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#if activityOpen}
|
||||||
|
<div class="min-h-0 flex-1 overflow-hidden">
|
||||||
|
<ActivityTimeline entries={$activityLogStore} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Pane>
|
||||||
|
</Splitpanes>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
4
web/src/lib/shims/app-environment.ts
Normal file
4
web/src/lib/shims/app-environment.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Shim for SvelteKit's `$app/environment` module — this is a plain Vite app,
|
||||||
|
// not SvelteKit, but svelte-splitpanes imports `browser` from it for its
|
||||||
|
// browser-detection utility. Aliased in vite.config.ts.
|
||||||
|
export const browser = typeof window !== 'undefined'
|
||||||
@@ -3,6 +3,7 @@ import type { ProxyOptions } from 'vite'
|
|||||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import { readFileSync, existsSync } from 'fs'
|
import { readFileSync, existsSync } from 'fs'
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
// In Docker, VERSION is copied into the build WORKDIR (/build/web/VERSION).
|
// In Docker, VERSION is copied into the build WORKDIR (/build/web/VERSION).
|
||||||
@@ -45,12 +46,27 @@ export default defineConfig({
|
|||||||
__OIKOS_DEV_TOKEN__: JSON.stringify(process.env.OIKOS_API_TOKEN ?? ''),
|
__OIKOS_DEV_TOKEN__: JSON.stringify(process.env.OIKOS_API_TOKEN ?? ''),
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: { $lib: '/src/lib' }
|
alias: {
|
||||||
|
$lib: '/src/lib',
|
||||||
|
// svelte-splitpanes imports SvelteKit's browser-detection module; this
|
||||||
|
// isn't a SvelteKit app, so point it at a plain shim (see the file).
|
||||||
|
// Needs a real filesystem path (not the /src/... shorthand $lib uses)
|
||||||
|
// so esbuild's dependency pre-bundler can resolve it too.
|
||||||
|
'$app/environment': fileURLToPath(new URL('./src/lib/shims/app-environment.ts', import.meta.url))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
emptyOutDir: true
|
emptyOutDir: true
|
||||||
},
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
// svelte-splitpanes imports SvelteKit's $app/environment (aliased above
|
||||||
|
// to a shim), but esbuild's dependency pre-bundler resolves that
|
||||||
|
// differently and fails before the dev server even starts — skip
|
||||||
|
// pre-bundling it so it goes through Vite's normal (alias-aware)
|
||||||
|
// transform pipeline instead.
|
||||||
|
exclude: ['svelte-splitpanes']
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': authProxy('http://localhost:8090'),
|
'/api': authProxy('http://localhost:8090'),
|
||||||
|
|||||||
Reference in New Issue
Block a user