diff options
| author | soryu <soryu@soryu.co> | 2026-05-02 15:07:33 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-02 15:07:33 +0100 |
| commit | 760516b2e7b97fa389fb3902e8d2314eea052ff0 (patch) | |
| tree | 69eb1dd212ef924ee9e451d8d88806f899c03e84 /makima/frontend | |
| parent | e11759447b1ac00becfb1e979e488f7f9c9cf478 (diff) | |
| download | soryu-760516b2e7b97fa389fb3902e8d2314eea052ff0.tar.gz soryu-760516b2e7b97fa389fb3902e8d2314eea052ff0.zip | |
feat: multi-document directives with ephemeral task lifecycle (#119)
* feat: soryu-co/soryu - makima: Fix folder/file naming and breadcrumb hash bugs
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Frontend: render multiple documents per directive folder
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* Fix DirectiveRevision import in openapi.rs after merge
* Fix document-directives.tsx merge artifacts and add inactive status
Diffstat (limited to 'makima/frontend')
| -rw-r--r-- | makima/frontend/pnpm-lock.yaml | 1913 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 114 | ||||
| -rw-r--r-- | makima/frontend/src/routes/document-directives.tsx | 2352 | ||||
| -rw-r--r-- | makima/frontend/tsconfig.tsbuildinfo | 2 |
4 files changed, 2592 insertions, 1789 deletions
diff --git a/makima/frontend/pnpm-lock.yaml b/makima/frontend/pnpm-lock.yaml index 2dfbf67..d19a0cf 100644 --- a/makima/frontend/pnpm-lock.yaml +++ b/makima/frontend/pnpm-lock.yaml @@ -48,34 +48,78 @@ dependencies: specifier: ^3.6.0 version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.5)(react-is@19.2.5)(react@19.2.5)(redux@5.0.1) -devDependencies: - '@react-router/dev': - specifier: ^7.1.0 - version: 7.14.2(react-router@7.14.2)(typescript@5.9.3)(vite@6.4.2) - '@react-router/fs-routes': - specifier: ^7.1.0 - version: 7.14.2(@react-router/dev@7.14.2)(typescript@5.9.3) - '@tailwindcss/vite': - specifier: ^4.0.0 - version: 4.2.4(vite@6.4.2) - '@types/react': - specifier: ^19.0.1 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.0.1 - version: 19.2.3(@types/react@19.2.14) - '@vitejs/plugin-react': - specifier: ^4.3.4 - version: 4.7.0(vite@6.4.2) - tailwindcss: - specifier: ^4.0.0 - version: 4.2.4 - typescript: - specifier: ^5.7.2 - version: 5.9.3 - vite: - specifier: ^6.0.5 - version: 6.4.2 + .: + dependencies: + '@lexical/history': + specifier: ^0.44.0 + version: 0.44.0 + '@lexical/list': + specifier: ^0.44.0 + version: 0.44.0 + '@lexical/markdown': + specifier: ^0.44.0 + version: 0.44.0 + '@lexical/react': + specifier: ^0.44.0 + version: 0.44.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.30) + '@lexical/rich-text': + specifier: ^0.44.0 + version: 0.44.0 + '@lexical/selection': + specifier: ^0.44.0 + version: 0.44.0 + '@lexical/utils': + specifier: ^0.44.0 + version: 0.44.0 + '@supabase/supabase-js': + specifier: ^2.90.1 + version: 2.95.2 + '@xyflow/react': + specifier: ^12.10.0 + version: 12.10.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + lexical: + specifier: ^0.44.0 + version: 0.44.0 + react: + specifier: ^19.0.0 + version: 19.2.4 + react-dom: + specifier: ^19.0.0 + version: 19.2.4(react@19.2.4) + react-router: + specifier: ^7.1.0 + version: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + recharts: + specifier: ^3.6.0 + version: 3.7.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.4)(react@19.2.4)(redux@5.0.1) + devDependencies: + '@react-router/dev': + specifier: ^7.1.0 + version: 7.13.0(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2)) + '@react-router/fs-routes': + specifier: ^7.1.0 + version: 7.13.0(@react-router/dev@7.13.0(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2)))(typescript@5.9.3) + '@tailwindcss/vite': + specifier: ^4.0.0 + version: 4.1.18(vite@6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2)) + '@types/react': + specifier: ^19.0.1 + version: 19.2.13 + '@types/react-dom': + specifier: ^19.0.1 + version: 19.2.3(@types/react@19.2.13) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2)) + tailwindcss: + specifier: ^4.0.0 + version: 4.1.18 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vite: + specifier: ^6.0.5 + version: 6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2) packages: @@ -620,48 +664,49 @@ packages: dev: true optional: true - /@floating-ui/core@1.7.5: + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - dependencies: - '@floating-ui/utils': 0.2.11 - dev: false - /@floating-ui/dom@1.7.6: + '@floating-ui/dom@1.7.6': resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 - dev: false - /@floating-ui/react-dom@2.1.8(react-dom@19.2.5)(react@19.2.5): + '@floating-ui/react-dom@2.1.8': resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - dependencies: - '@floating-ui/dom': 1.7.6 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - dev: false - /@floating-ui/react@0.27.19(react-dom@19.2.5)(react@19.2.5): + '@floating-ui/react@0.27.19': resolution: {integrity: sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==} peerDependencies: react: '>=17.0.0' react-dom: '>=17.0.0' - dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5)(react@19.2.5) - '@floating-ui/utils': 0.2.11 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - tabbable: 6.4.0 - dev: false - /@floating-ui/utils@0.2.11: + '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - dev: false - /@jridgewell/gen-mapping@0.3.13: + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.27.19': + resolution: {integrity: sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -691,133 +736,52 @@ packages: '@jridgewell/sourcemap-codec': 1.5.5 dev: true - /@lexical/clipboard@0.44.0: + '@lexical/clipboard@0.44.0': resolution: {integrity: sha512-nfmNIs7uENqlDI7cm2E4I1Yp8mDJGMhEQIrIV2rNWnL1oeHVXQ7yuYdyoPdcY1zuj/9nvkYBQYUEh0QiGwpETA==} - dependencies: - '@lexical/extension': 0.44.0 - '@lexical/html': 0.44.0 - '@lexical/list': 0.44.0 - '@lexical/selection': 0.44.0 - '@lexical/utils': 0.44.0 - '@types/trusted-types': 2.0.7 - lexical: 0.44.0 - dev: false - /@lexical/code-core@0.44.0: + '@lexical/code-core@0.44.0': resolution: {integrity: sha512-m57JyXTIvW1tsqw/Vuogk8jqWCZZIeFQbWybRc46ytR8ReDgzPRODpN8+dacIIeRH5yC5UC3lAa743mtdNkxqg==} - dependencies: - '@lexical/extension': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/devtools-core@0.44.0(react-dom@19.2.5)(react@19.2.5): + '@lexical/devtools-core@0.44.0': resolution: {integrity: sha512-X3uNG3P1vOsdzmEcy+7m9DxAcIVtVUZnvskmLqqLs6VluVVwH9xy7h1bPsvlDKvj1Nj73tWJ3TW0qXQWDTo5tw==} peerDependencies: react: '>=17.x' react-dom: '>=17.x' - dependencies: - '@lexical/html': 0.44.0 - '@lexical/link': 0.44.0 - '@lexical/mark': 0.44.0 - '@lexical/table': 0.44.0 - '@lexical/utils': 0.44.0 - lexical: 0.44.0 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - dev: false - /@lexical/dragon@0.44.0: + '@lexical/dragon@0.44.0': resolution: {integrity: sha512-RhlsjVDket9k1+YFEkDE0/7Qyrh2BI0vxBMzrWwPJTXX/4YFanYN9su8RSabkIukBBJ3QiNOOoC8FKK4Lkr4qg==} - dependencies: - '@lexical/extension': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/extension@0.44.0: + '@lexical/extension@0.44.0': resolution: {integrity: sha512-BsYtoc+0EU0pqcOpf/lIUDU6LQVO6zX2AawZoUWJzT3Wzfov23qsqZWvl2WGM9dnRTN5iISJL3Fl53bQVxiXxw==} - dependencies: - '@lexical/utils': 0.44.0 - '@preact/signals-core': 1.14.1 - lexical: 0.44.0 - dev: false - /@lexical/hashtag@0.44.0: + '@lexical/hashtag@0.44.0': resolution: {integrity: sha512-0WATahDSqYKVTudQv3KpFbLeCpmrCpRptPFbjxOMckAX2MRpYlrExlqKfgfpri5BSQPtG49EPSGeNfSx/Faavw==} - dependencies: - '@lexical/text': 0.44.0 - '@lexical/utils': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/history@0.44.0: + '@lexical/history@0.44.0': resolution: {integrity: sha512-RGXcbFTgYL1GIWaReBI26mNSsJTfiA9EAtDY4LBeZ14NrIQhYNokKgNiOxq5Bn8xXrl2+mawQEqoMfgpWp/5YA==} - dependencies: - '@lexical/extension': 0.44.0 - '@lexical/utils': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/html@0.44.0: + '@lexical/html@0.44.0': resolution: {integrity: sha512-5X6eGsgwtqPxABsuShUxF7ZfyB/U4GwSEyeonvwH1Vc/5Q2uQVjlB+FAYd+MNwWMHMh4d4+yZ3l70AtIuhr5eg==} - dependencies: - '@lexical/extension': 0.44.0 - '@lexical/selection': 0.44.0 - '@lexical/utils': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/link@0.44.0: + '@lexical/link@0.44.0': resolution: {integrity: sha512-uvEqEol/mLEzGVQd8Rok9I48RgYPKokM/nsclI9nYcEdccVOM2Nri4ntoRwodhbccFLtjMPl8OBldwXbfc77tQ==} - dependencies: - '@lexical/extension': 0.44.0 - '@lexical/utils': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/list@0.44.0: + '@lexical/list@0.44.0': resolution: {integrity: sha512-ZTCWxDz1okPrC9FBXi1yV3W5fbQQeMUlFIcSVF9HibcVPmCsPa900IxthuiQbGiTycUyXDTOB3IUYRtlJNtpjw==} - dependencies: - '@lexical/extension': 0.44.0 - '@lexical/utils': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/mark@0.44.0: + '@lexical/mark@0.44.0': resolution: {integrity: sha512-bWMowllwe6BcgYMAkrsZx6Z+CX/72qCQpFKhlkR4ael92yOWSBkz68xp1wxxkSnQX9zoI1gYTeWBofVsSDKcsQ==} - dependencies: - '@lexical/utils': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/markdown@0.44.0: + '@lexical/markdown@0.44.0': resolution: {integrity: sha512-DwlXdp85pYMo3exDF6W3iz8plpuP+RQ4Me4Iljm7O5aPDp0SSrIoZxyX4zS668mVAoz5HHj1Ka0kQkft8mq26Q==} - dependencies: - '@lexical/code-core': 0.44.0 - '@lexical/link': 0.44.0 - '@lexical/list': 0.44.0 - '@lexical/rich-text': 0.44.0 - '@lexical/text': 0.44.0 - '@lexical/utils': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/overflow@0.44.0: + '@lexical/overflow@0.44.0': resolution: {integrity: sha512-5GYaYjSxn27pqHRfU+tQ2STF10wgJvI+MUnwTnUFSzy3dko1b+oV94K/Yx0TuEewPbwDibfoFA8CwqUvOLHAyw==} - dependencies: - lexical: 0.44.0 - dev: false - /@lexical/plain-text@0.44.0: + '@lexical/plain-text@0.44.0': resolution: {integrity: sha512-bIV4Lljk0x70zFhkZIwzSPK5q3m9FpDisjGm2/3Q/chb+5BW3Tv8QJmqnpCiSO6S2KXO7gfSy81ZfkQ1dcd4EQ==} - dependencies: - '@lexical/clipboard': 0.44.0 - '@lexical/dragon': 0.44.0 - '@lexical/selection': 0.44.0 - '@lexical/utils': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/react@0.44.0(react-dom@19.2.5)(react@19.2.5)(yjs@13.6.30): + '@lexical/react@0.44.0': resolution: {integrity: sha512-p/NQd/fMh3pXb1XqegE2ruvWDcUmfB12OidQ9nwtMtj5VfcUjQu2I+trUhgGRIADxSYxMWmw+8PPj5YSf4m5oA==} peerDependencies: react: '>=17.x' @@ -826,89 +790,114 @@ packages: peerDependenciesMeta: yjs: optional: true - dependencies: - '@floating-ui/react': 0.27.19(react-dom@19.2.5)(react@19.2.5) - '@lexical/devtools-core': 0.44.0(react-dom@19.2.5)(react@19.2.5) - '@lexical/dragon': 0.44.0 - '@lexical/extension': 0.44.0 - '@lexical/hashtag': 0.44.0 - '@lexical/history': 0.44.0 - '@lexical/link': 0.44.0 - '@lexical/list': 0.44.0 - '@lexical/mark': 0.44.0 - '@lexical/markdown': 0.44.0 - '@lexical/overflow': 0.44.0 - '@lexical/plain-text': 0.44.0 - '@lexical/rich-text': 0.44.0 - '@lexical/table': 0.44.0 - '@lexical/text': 0.44.0 - '@lexical/utils': 0.44.0 - '@lexical/yjs': 0.44.0(yjs@13.6.30) - lexical: 0.44.0 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - react-error-boundary: 6.1.1(react@19.2.5) - yjs: 13.6.30 - dev: false - /@lexical/rich-text@0.44.0: + '@lexical/rich-text@0.44.0': resolution: {integrity: sha512-IIdrutK5GY47ITjPlZB7KzUi9dBDwygsyFOwolnrYSL7m6TtGhAqrYiFg/YNOTT/nBzK3KQeCJRbnxpjJAVZtQ==} - dependencies: - '@lexical/clipboard': 0.44.0 - '@lexical/dragon': 0.44.0 - '@lexical/selection': 0.44.0 - '@lexical/utils': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/selection@0.44.0: + '@lexical/selection@0.44.0': resolution: {integrity: sha512-AEyeZJFFr5YRLeqVR+X0QAW19c4Fk4MFAQu52z2gxAyDGTj9xwVJxjfepVpfUp4P9K+sPtJ/yaqfMXH506ksSQ==} - dependencies: - lexical: 0.44.0 - dev: false - /@lexical/table@0.44.0: + '@lexical/table@0.44.0': resolution: {integrity: sha512-5Uq0O/fBCxcZp9y17fXUONY7dU9lVo/mB5JHy23laIiKzBKP5IzzTLMU9ikZTppIXbMNxYXd+R2pmy7PYTLyvw==} - dependencies: - '@lexical/clipboard': 0.44.0 - '@lexical/extension': 0.44.0 - '@lexical/utils': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/text@0.44.0: + '@lexical/text@0.44.0': resolution: {integrity: sha512-1XJD8ZbwaXljTl8k4+jjiopdhnYZm26IJw9Gv8+cIThVC0b6B3JZ/WxH97BMDcSloKvWHFkGiPztxRwNwA29Rw==} - dependencies: - lexical: 0.44.0 - dev: false - /@lexical/utils@0.44.0: + '@lexical/utils@0.44.0': resolution: {integrity: sha512-/D2ptztNevfBJgtkj4uaiYBeRcvSy+1mQj6pNYaCFZIoPJIwl6H5fXwWAvpvr11vcQKP9DEEoXR+V4qkMOA+EA==} - dependencies: - '@lexical/selection': 0.44.0 - lexical: 0.44.0 - dev: false - /@lexical/yjs@0.44.0(yjs@13.6.30): + '@lexical/yjs@0.44.0': resolution: {integrity: sha512-b3QTub9J/3LuwSSdooynb6GbMHBRyBT4xUbXzXqNPbDHgYe6CDrqf/uJIHRihIjAhOnPaHYqo9XUzitl++N1DQ==} peerDependencies: yjs: '>=13.5.22' - dependencies: - '@lexical/selection': 0.44.0 - lexical: 0.44.0 - yjs: 13.6.30 - dev: false - /@mjackson/node-fetch-server@0.2.0: + '@lexical/clipboard@0.44.0': + resolution: {integrity: sha512-nfmNIs7uENqlDI7cm2E4I1Yp8mDJGMhEQIrIV2rNWnL1oeHVXQ7yuYdyoPdcY1zuj/9nvkYBQYUEh0QiGwpETA==} + + '@lexical/code-core@0.44.0': + resolution: {integrity: sha512-m57JyXTIvW1tsqw/Vuogk8jqWCZZIeFQbWybRc46ytR8ReDgzPRODpN8+dacIIeRH5yC5UC3lAa743mtdNkxqg==} + + '@lexical/devtools-core@0.44.0': + resolution: {integrity: sha512-X3uNG3P1vOsdzmEcy+7m9DxAcIVtVUZnvskmLqqLs6VluVVwH9xy7h1bPsvlDKvj1Nj73tWJ3TW0qXQWDTo5tw==} + peerDependencies: + react: '>=17.x' + react-dom: '>=17.x' + + '@lexical/dragon@0.44.0': + resolution: {integrity: sha512-RhlsjVDket9k1+YFEkDE0/7Qyrh2BI0vxBMzrWwPJTXX/4YFanYN9su8RSabkIukBBJ3QiNOOoC8FKK4Lkr4qg==} + + '@lexical/extension@0.44.0': + resolution: {integrity: sha512-BsYtoc+0EU0pqcOpf/lIUDU6LQVO6zX2AawZoUWJzT3Wzfov23qsqZWvl2WGM9dnRTN5iISJL3Fl53bQVxiXxw==} + + '@lexical/hashtag@0.44.0': + resolution: {integrity: sha512-0WATahDSqYKVTudQv3KpFbLeCpmrCpRptPFbjxOMckAX2MRpYlrExlqKfgfpri5BSQPtG49EPSGeNfSx/Faavw==} + + '@lexical/history@0.44.0': + resolution: {integrity: sha512-RGXcbFTgYL1GIWaReBI26mNSsJTfiA9EAtDY4LBeZ14NrIQhYNokKgNiOxq5Bn8xXrl2+mawQEqoMfgpWp/5YA==} + + '@lexical/html@0.44.0': + resolution: {integrity: sha512-5X6eGsgwtqPxABsuShUxF7ZfyB/U4GwSEyeonvwH1Vc/5Q2uQVjlB+FAYd+MNwWMHMh4d4+yZ3l70AtIuhr5eg==} + + '@lexical/link@0.44.0': + resolution: {integrity: sha512-uvEqEol/mLEzGVQd8Rok9I48RgYPKokM/nsclI9nYcEdccVOM2Nri4ntoRwodhbccFLtjMPl8OBldwXbfc77tQ==} + + '@lexical/list@0.44.0': + resolution: {integrity: sha512-ZTCWxDz1okPrC9FBXi1yV3W5fbQQeMUlFIcSVF9HibcVPmCsPa900IxthuiQbGiTycUyXDTOB3IUYRtlJNtpjw==} + + '@lexical/mark@0.44.0': + resolution: {integrity: sha512-bWMowllwe6BcgYMAkrsZx6Z+CX/72qCQpFKhlkR4ael92yOWSBkz68xp1wxxkSnQX9zoI1gYTeWBofVsSDKcsQ==} + + '@lexical/markdown@0.44.0': + resolution: {integrity: sha512-DwlXdp85pYMo3exDF6W3iz8plpuP+RQ4Me4Iljm7O5aPDp0SSrIoZxyX4zS668mVAoz5HHj1Ka0kQkft8mq26Q==} + + '@lexical/overflow@0.44.0': + resolution: {integrity: sha512-5GYaYjSxn27pqHRfU+tQ2STF10wgJvI+MUnwTnUFSzy3dko1b+oV94K/Yx0TuEewPbwDibfoFA8CwqUvOLHAyw==} + + '@lexical/plain-text@0.44.0': + resolution: {integrity: sha512-bIV4Lljk0x70zFhkZIwzSPK5q3m9FpDisjGm2/3Q/chb+5BW3Tv8QJmqnpCiSO6S2KXO7gfSy81ZfkQ1dcd4EQ==} + + '@lexical/react@0.44.0': + resolution: {integrity: sha512-p/NQd/fMh3pXb1XqegE2ruvWDcUmfB12OidQ9nwtMtj5VfcUjQu2I+trUhgGRIADxSYxMWmw+8PPj5YSf4m5oA==} + peerDependencies: + react: '>=17.x' + react-dom: '>=17.x' + yjs: '>=13.5.22' + peerDependenciesMeta: + yjs: + optional: true + + '@lexical/rich-text@0.44.0': + resolution: {integrity: sha512-IIdrutK5GY47ITjPlZB7KzUi9dBDwygsyFOwolnrYSL7m6TtGhAqrYiFg/YNOTT/nBzK3KQeCJRbnxpjJAVZtQ==} + + '@lexical/selection@0.44.0': + resolution: {integrity: sha512-AEyeZJFFr5YRLeqVR+X0QAW19c4Fk4MFAQu52z2gxAyDGTj9xwVJxjfepVpfUp4P9K+sPtJ/yaqfMXH506ksSQ==} + + '@lexical/table@0.44.0': + resolution: {integrity: sha512-5Uq0O/fBCxcZp9y17fXUONY7dU9lVo/mB5JHy23laIiKzBKP5IzzTLMU9ikZTppIXbMNxYXd+R2pmy7PYTLyvw==} + + '@lexical/text@0.44.0': + resolution: {integrity: sha512-1XJD8ZbwaXljTl8k4+jjiopdhnYZm26IJw9Gv8+cIThVC0b6B3JZ/WxH97BMDcSloKvWHFkGiPztxRwNwA29Rw==} + + '@lexical/utils@0.44.0': + resolution: {integrity: sha512-/D2ptztNevfBJgtkj4uaiYBeRcvSy+1mQj6pNYaCFZIoPJIwl6H5fXwWAvpvr11vcQKP9DEEoXR+V4qkMOA+EA==} + + '@lexical/yjs@0.44.0': + resolution: {integrity: sha512-b3QTub9J/3LuwSSdooynb6GbMHBRyBT4xUbXzXqNPbDHgYe6CDrqf/uJIHRihIjAhOnPaHYqo9XUzitl++N1DQ==} + peerDependencies: + yjs: '>=13.5.22' + + '@mjackson/node-fetch-server@0.2.0': resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==} dev: true - /@preact/signals-core@1.14.1: + '@preact/signals-core@1.14.1': + resolution: {integrity: sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng==} + + '@preact/signals-core@1.14.1': resolution: {integrity: sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng==} - dev: false - /@react-router/dev@7.14.2(react-router@7.14.2)(typescript@5.9.3)(vite@6.4.2): - resolution: {integrity: sha512-lU88Ls4iC78RdPOKkER54+hlsHzzS8WSZrf2/cGQumbIN2A5WvO0LDyv72cdJmLWujgZ9rpNoGzmqWINssShGQ==} + '@react-router/dev@7.13.0': + resolution: {integrity: sha512-0vRfTrS6wIXr9j0STu614Cv2ytMr21evnv1r+DXPv5cJ4q0V2x2kBAXC8TAqEXkpN5vdhbXBlbGQ821zwOfhvg==} engines: {node: '>=20.0.0'} hasBin: true peerDependencies: @@ -1089,105 +1078,66 @@ packages: resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} cpu: [arm] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-linux-arm-musleabihf@4.60.2: resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} cpu: [arm] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-linux-arm64-gnu@4.60.2: resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-linux-arm64-musl@4.60.2: resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-linux-loong64-gnu@4.60.2: resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} cpu: [loong64] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-linux-loong64-musl@4.60.2: resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} cpu: [loong64] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-linux-ppc64-gnu@4.60.2: resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} cpu: [ppc64] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-linux-ppc64-musl@4.60.2: resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} cpu: [ppc64] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-linux-riscv64-gnu@4.60.2: resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} cpu: [riscv64] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-linux-riscv64-musl@4.60.2: resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} cpu: [riscv64] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-linux-s390x-gnu@4.60.2: resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} cpu: [s390x] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-linux-x64-gnu@4.60.2: resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-linux-x64-musl@4.60.2: resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true /@rollup/rollup-openbsd-x64@4.60.2: resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} @@ -1367,36 +1317,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true /@tailwindcss/oxide-linux-arm64-musl@4.2.4: resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true /@tailwindcss/oxide-linux-x64-gnu@4.2.4: resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true /@tailwindcss/oxide-linux-x64-musl@4.2.4: resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true /@tailwindcss/oxide-wasm32-wasi@4.2.4: resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} @@ -1577,11 +1515,13 @@ packages: dependencies: csstype: 3.2.3 - /@types/trusted-types@2.0.7: + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - dev: false - /@types/use-sync-external-store@0.0.6: + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} dev: false @@ -1989,11 +1929,13 @@ packages: engines: {node: '>=18'} dev: true - /isomorphic.js@0.2.5: + isomorphic.js@0.2.5: resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} - dev: false - /jiti@2.6.1: + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true dev: true @@ -2014,20 +1956,24 @@ packages: hasBin: true dev: true - /lexical@0.44.0: + lexical@0.44.0: resolution: {integrity: sha512-ReDUjRlFgkGoPWzvdjr7s16PUVpHATN+2NH2NiZs+PLlISTaIFFgKil2P467oP3Vg+XgmpDsUgmWZsFJTztYjg==} - dev: false - /lib0@0.2.117: + lib0@0.2.117: + resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} + engines: {node: '>=16'} + hasBin: true + + lexical@0.44.0: + resolution: {integrity: sha512-ReDUjRlFgkGoPWzvdjr7s16PUVpHATN+2NH2NiZs+PLlISTaIFFgKil2P467oP3Vg+XgmpDsUgmWZsFJTztYjg==} + + lib0@0.2.117: resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} engines: {node: '>=16'} hasBin: true - dependencies: - isomorphic.js: 0.2.5 - dev: false - /lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] @@ -2076,36 +2022,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true /lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true /lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true /lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true /lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -2234,13 +2168,18 @@ packages: react: 19.2.5 scheduler: 0.27.0 - /react-error-boundary@6.1.1(react@19.2.5): + react-error-boundary@6.1.1: + resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + react-error-boundary@6.1.1: resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==} peerDependencies: react: ^18.0.0 || ^19.0.0 - dependencies: - react: 19.2.5 - dev: false + + react-is@19.2.4: + resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==} /react-is@19.2.5: resolution: {integrity: sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==} @@ -2399,9 +2338,14 @@ packages: engines: {node: '>=0.10.0'} dev: true - /tabbable@6.4.0: + tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} - dev: false + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} /tailwindcss@4.2.4: resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==} @@ -2579,14 +2523,15 @@ packages: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} dev: true - /yjs@13.6.30: + yjs@13.6.30: resolution: {integrity: sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} - dependencies: - lib0: 0.2.117 - dev: false - /zustand@4.5.7(@types/react@19.2.14)(react@19.2.5): + yjs@13.6.30: + resolution: {integrity: sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + + zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} peerDependencies: @@ -2601,7 +2546,1365 @@ packages: react: optional: true dependencies: - '@types/react': 19.2.14 - react: 19.2.5 - use-sync-external-store: 1.6.0(react@19.2.5) - dev: false + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.0.2 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@floating-ui/react@0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/utils': 0.2.11 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tabbable: 6.4.0 + + '@floating-ui/utils@0.2.11': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lexical/clipboard@0.44.0': + dependencies: + '@lexical/extension': 0.44.0 + '@lexical/html': 0.44.0 + '@lexical/list': 0.44.0 + '@lexical/selection': 0.44.0 + '@lexical/utils': 0.44.0 + '@types/trusted-types': 2.0.7 + lexical: 0.44.0 + + '@lexical/code-core@0.44.0': + dependencies: + '@lexical/extension': 0.44.0 + lexical: 0.44.0 + + '@lexical/devtools-core@0.44.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@lexical/html': 0.44.0 + '@lexical/link': 0.44.0 + '@lexical/mark': 0.44.0 + '@lexical/table': 0.44.0 + '@lexical/utils': 0.44.0 + lexical: 0.44.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@lexical/dragon@0.44.0': + dependencies: + '@lexical/extension': 0.44.0 + lexical: 0.44.0 + + '@lexical/extension@0.44.0': + dependencies: + '@lexical/utils': 0.44.0 + '@preact/signals-core': 1.14.1 + lexical: 0.44.0 + + '@lexical/hashtag@0.44.0': + dependencies: + '@lexical/text': 0.44.0 + '@lexical/utils': 0.44.0 + lexical: 0.44.0 + + '@lexical/history@0.44.0': + dependencies: + '@lexical/extension': 0.44.0 + '@lexical/utils': 0.44.0 + lexical: 0.44.0 + + '@lexical/html@0.44.0': + dependencies: + '@lexical/extension': 0.44.0 + '@lexical/selection': 0.44.0 + '@lexical/utils': 0.44.0 + lexical: 0.44.0 + + '@lexical/link@0.44.0': + dependencies: + '@lexical/extension': 0.44.0 + '@lexical/utils': 0.44.0 + lexical: 0.44.0 + + '@lexical/list@0.44.0': + dependencies: + '@lexical/extension': 0.44.0 + '@lexical/utils': 0.44.0 + lexical: 0.44.0 + + '@lexical/mark@0.44.0': + dependencies: + '@lexical/utils': 0.44.0 + lexical: 0.44.0 + + '@lexical/markdown@0.44.0': + dependencies: + '@lexical/code-core': 0.44.0 + '@lexical/link': 0.44.0 + '@lexical/list': 0.44.0 + '@lexical/rich-text': 0.44.0 + '@lexical/text': 0.44.0 + '@lexical/utils': 0.44.0 + lexical: 0.44.0 + + '@lexical/overflow@0.44.0': + dependencies: + lexical: 0.44.0 + + '@lexical/plain-text@0.44.0': + dependencies: + '@lexical/clipboard': 0.44.0 + '@lexical/dragon': 0.44.0 + '@lexical/selection': 0.44.0 + '@lexical/utils': 0.44.0 + lexical: 0.44.0 + + '@lexical/react@0.44.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.30)': + dependencies: + '@floating-ui/react': 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@lexical/devtools-core': 0.44.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@lexical/dragon': 0.44.0 + '@lexical/extension': 0.44.0 + '@lexical/hashtag': 0.44.0 + '@lexical/history': 0.44.0 + '@lexical/link': 0.44.0 + '@lexical/list': 0.44.0 + '@lexical/mark': 0.44.0 + '@lexical/markdown': 0.44.0 + '@lexical/overflow': 0.44.0 + '@lexical/plain-text': 0.44.0 + '@lexical/rich-text': 0.44.0 + '@lexical/table': 0.44.0 + '@lexical/text': 0.44.0 + '@lexical/utils': 0.44.0 + '@lexical/yjs': 0.44.0(yjs@13.6.30) + lexical: 0.44.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-error-boundary: 6.1.1(react@19.2.4) + optionalDependencies: + yjs: 13.6.30 + + '@lexical/rich-text@0.44.0': + dependencies: + '@lexical/clipboard': 0.44.0 + '@lexical/dragon': 0.44.0 + '@lexical/selection': 0.44.0 + '@lexical/utils': 0.44.0 + lexical: 0.44.0 + + '@lexical/selection@0.44.0': + dependencies: + lexical: 0.44.0 + + '@lexical/table@0.44.0': + dependencies: + '@lexical/clipboard': 0.44.0 + '@lexical/extension': 0.44.0 + '@lexical/utils': 0.44.0 + lexical: 0.44.0 + + '@lexical/text@0.44.0': + dependencies: + lexical: 0.44.0 + + '@lexical/utils@0.44.0': + dependencies: + '@lexical/selection': 0.44.0 + lexical: 0.44.0 + + '@lexical/yjs@0.44.0(yjs@13.6.30)': + dependencies: + '@lexical/selection': 0.44.0 + lexical: 0.44.0 + yjs: 13.6.30 + + '@mjackson/node-fetch-server@0.2.0': {} + + '@preact/signals-core@1.14.1': {} + + '@react-router/dev@7.13.0(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.0 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@react-router/node': 7.13.0(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@remix-run/node-fetch-server': 0.13.0 + arg: 5.0.2 + babel-dead-code-elimination: 1.0.12 + chokidar: 4.0.3 + dedent: 1.7.1 + es-module-lexer: 1.7.0 + exit-hook: 2.2.1 + isbot: 5.1.34 + jsesc: 3.0.2 + lodash: 4.17.23 + p-map: 7.0.4 + pathe: 1.1.2 + picocolors: 1.1.1 + pkg-types: 2.3.0 + prettier: 3.8.1 + react-refresh: 0.14.2 + react-router: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + semver: 7.7.4 + tinyglobby: 0.2.15 + valibot: 1.2.0(typescript@5.9.3) + vite: 6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2) + vite-node: 3.2.4(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + '@react-router/fs-routes@7.13.0(@react-router/dev@7.13.0(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2)))(typescript@5.9.3)': + dependencies: + '@react-router/dev': 7.13.0(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2)) + minimatch: 9.0.5 + optionalDependencies: + typescript: 5.9.3 + + '@react-router/node@7.13.0(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + dependencies: + '@mjackson/node-fetch-server': 0.2.0 + react-router: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + optionalDependencies: + typescript: 5.9.3 + + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.13)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.3 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.4 + react-redux: 9.2.0(@types/react@19.2.13)(react@19.2.4)(redux@5.0.1) + + '@remix-run/node-fetch-server@0.13.0': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@supabase/auth-js@2.95.2': + dependencies: + tslib: 2.8.1 + + '@supabase/functions-js@2.95.2': + dependencies: + tslib: 2.8.1 + + '@supabase/postgrest-js@2.95.2': + dependencies: + tslib: 2.8.1 + + '@supabase/realtime-js@2.95.2': + dependencies: + '@types/phoenix': 1.6.7 + '@types/ws': 8.18.1 + tslib: 2.8.1 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/storage-js@2.95.2': + dependencies: + iceberg-js: 0.8.1 + tslib: 2.8.1 + + '@supabase/supabase-js@2.95.2': + dependencies: + '@supabase/auth-js': 2.95.2 + '@supabase/functions-js': 2.95.2 + '@supabase/postgrest-js': 2.95.2 + '@supabase/realtime-js': 2.95.2 + '@supabase/storage-js': 2.95.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.19.0 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/vite@4.1.18(vite@6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + tailwindcss: 4.1.18 + vite: 6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2) + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/estree@1.0.8': {} + + '@types/node@25.2.1': + dependencies: + undici-types: 7.16.0 + + '@types/phoenix@1.6.7': {} + + '@types/react-dom@19.2.3(@types/react@19.2.13)': + dependencies: + '@types/react': 19.2.13 + + '@types/react@19.2.13': + dependencies: + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': {} + + '@types/use-sync-external-store@0.0.6': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.2.1 + + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2) + transitivePeerDependencies: + - supports-color + + '@xyflow/react@12.10.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@xyflow/system': 0.0.74 + classcat: 5.0.5 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + zustand: 4.5.7(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.74': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + + arg@5.0.2: {} + + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.9.19: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001768 + electron-to-chromium: 1.5.286 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + cac@6.7.14: {} + + caniuse-lite@1.0.30001768: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + classcat@5.0.5: {} + + clsx@2.1.1: {} + + confbox@0.2.2: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js-light@2.5.1: {} + + dedent@1.7.1: {} + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.286: {} + + enhanced-resolve@5.19.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + es-module-lexer@1.7.0: {} + + es-toolkit@1.44.0: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + eventemitter3@5.0.4: {} + + exit-hook@2.2.1: {} + + exsolve@1.0.8: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + graceful-fs@4.2.11: {} + + iceberg-js@0.8.1: {} + + immer@10.2.0: {} + + immer@11.1.3: {} + + internmap@2.0.3: {} + + isbot@5.1.34: {} + + isomorphic.js@0.2.5: {} + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + jsesc@3.0.2: {} + + json5@2.2.3: {} + + lexical@0.44.0: {} + + lib0@0.2.117: + dependencies: + isomorphic.js: 0.2.5 + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lodash@4.17.23: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-releases@2.0.27: {} + + p-map@7.0.4: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.8 + pathe: 2.0.3 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@3.8.1: {} + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-error-boundary@6.1.1(react@19.2.4): + dependencies: + react: 19.2.4 + + react-is@19.2.4: {} + + react-redux@9.2.0(@types/react@19.2.13)(react@19.2.4)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + redux: 5.0.1 + + react-refresh@0.14.2: {} + + react-refresh@0.17.0: {} + + react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + cookie: 1.1.1 + react: 19.2.4 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + + react@19.2.4: {} + + readdirp@4.1.2: {} + + recharts@3.7.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.4)(react@19.2.4)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.13)(react@19.2.4)(redux@5.0.1))(react@19.2.4) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.44.0 + eventemitter3: 5.0.4 + immer: 10.2.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-is: 19.2.4 + react-redux: 9.2.0(@types/react@19.2.13)(react@19.2.4)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.4) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + + reselect@5.1.1: {} + + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + set-cookie-parser@2.7.2: {} + + source-map-js@1.2.1: {} + + tabbable@6.4.0: {} + + tailwindcss@4.1.18: {} + + tapable@2.3.0: {} + + tiny-invariant@1.3.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + vite-node@3.2.4(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@6.4.1(@types/node@25.2.1)(jiti@2.6.1)(lightningcss@1.30.2): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.2.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + + ws@8.19.0: {} + + yallist@3.1.1: {} + + yjs@13.6.30: + dependencies: + lib0: 0.2.117 + + zustand@4.5.7(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + immer: 11.1.3 + react: 19.2.4 diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 4d664cf..80da511 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3583,6 +3583,120 @@ export async function pickUpOrders(directiveId: string): Promise<PickUpOrdersRes } // ============================================================================= +// Directive Documents API +// +// A directive_document is one of N markdown documents owned by a directive. +// Each has its own lifecycle (draft → active → shipped → archived) and may be +// attached to a PR. The frontend never calls the /ship endpoint — the backend +// invokes that itself when PRs are raised. Editing a shipped document +// auto-reactivates it on the backend (see PATCH handler). +// ============================================================================= + +export type DirectiveDocumentStatus = "draft" | "active" | "shipped" | "archived"; + +export interface DirectiveDocument { + id: string; + directiveId: string; + title: string; + body: string; + status: DirectiveDocumentStatus; + prUrl: string | null; + prBranch: string | null; + shippedAt: string | null; + archivedAt: string | null; + version: number; + createdAt: string; + updatedAt: string; +} + +export interface CreateDirectiveDocumentRequest { + title?: string; + body?: string; +} + +export interface UpdateDirectiveDocumentRequest { + title?: string; + body?: string; +} + +export async function listDirectiveDocuments(directiveId: string): Promise<DirectiveDocument[]> { + const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/documents`); + if (!res.ok) throw new Error(`Failed to list directive documents: ${res.statusText}`); + return res.json(); +} + +export async function createDirectiveDocument( + directiveId: string, + req: CreateDirectiveDocumentRequest = {}, +): Promise<DirectiveDocument> { + const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/documents`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(req), + }); + if (!res.ok) throw new Error(`Failed to create directive document: ${res.statusText}`); + return res.json(); +} + +export async function getDirectiveDocument(documentId: string): Promise<DirectiveDocument> { + const res = await authFetch(`${API_BASE}/api/v1/directive-documents/${documentId}`); + if (!res.ok) throw new Error(`Failed to get directive document: ${res.statusText}`); + return res.json(); +} + +/** + * Update a directive document's title and/or body. The backend auto-reactivates + * a shipped document when its body changes (re-stamps status to `active`), + * which is why we don't expose a separate "reactivate" call. + */ +export async function updateDirectiveDocument( + documentId: string, + req: UpdateDirectiveDocumentRequest, +): Promise<DirectiveDocument> { + const res = await authFetch(`${API_BASE}/api/v1/directive-documents/${documentId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(req), + }); + if (!res.ok) throw new Error(`Failed to update directive document: ${res.statusText}`); + return res.json(); +} + +export async function archiveDirectiveDocument(documentId: string): Promise<DirectiveDocument> { + const res = await authFetch(`${API_BASE}/api/v1/directive-documents/${documentId}/archive`, { + method: "POST", + }); + if (!res.ok) throw new Error(`Failed to archive directive document: ${res.statusText}`); + return res.json(); +} + +/** Steps and tasks attached to a single directive document. Drives the + * per-document `tasks/` subfolder in the sidebar — when the document + * ships, its tasks visually move with it under shipped/. */ +export interface DocumentTasksResponse { + steps: DirectiveStep[]; + tasks: Task[]; +} + +/** + * List the steps and ephemeral tasks attached to a specific directive + * document. Used by the sidebar to render a `tasks/` subfolder beside each + * document — including shipped documents, whose tasks remain attached so + * they continue to render under shipped/ alongside the document. + */ +export async function listDirectiveDocumentTasks( + documentId: string, +): Promise<DocumentTasksResponse> { + const res = await authFetch( + `${API_BASE}/api/v1/directive-documents/${documentId}/tasks`, + ); + if (!res.ok) { + throw new Error(`Failed to list directive document tasks: ${res.statusText}`); + } + return res.json(); +} + +// ============================================================================= // Directive Order Groups (DOGs) API // ============================================================================= diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx index a3ea969..b583bef 100644 --- a/makima/frontend/src/routes/document-directives.tsx +++ b/makima/frontend/src/routes/document-directives.tsx @@ -3,36 +3,20 @@ import { useNavigate, useParams, useSearchParams } from "react-router"; import { Masthead } from "../components/Masthead"; import { useDirective, useDirectives } from "../hooks/useDirectives"; import { useAuth } from "../contexts/AuthContext"; -import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext"; import { DocumentEditor } from "../components/directives/DocumentEditor"; -import { DocumentTaskStream } from "../components/directives/DocumentTaskStream"; -import { DirectiveContextMenu } from "../components/directives/DirectiveContextMenu"; import { - startDirective, - pauseDirective, - updateDirective, - deleteDirective, - completeDirectiveStep, - failDirectiveStep, - skipDirectiveStep, - stopTask, - listDirectiveRevisions, - newDirectiveDraft, - createDirectivePR, - advanceDirective, - cleanupDirective, - pickUpOrders, - sendTaskMessage, - listDirectiveEphemeralTasks, - createDirectiveTask, - listOrphanTasks, -} from "../lib/api"; -import type { - DirectiveStatus, - DirectiveSummary, - DirectiveWithSteps, - DirectiveRevision, - TaskSummary, + type DirectiveSummary, + type DirectiveStatus, + type DirectiveDocument, + type DirectiveDocumentStatus, + type DirectiveStep, + type Task, + type DocumentTasksResponse, + listDirectiveDocuments, + createDirectiveDocument, + getDirectiveDocument, + updateDirectiveDocument, + listDirectiveDocumentTasks, } from "../lib/api"; // Status dot color, matching the existing tabular UI's badge palette so the @@ -46,17 +30,52 @@ const STATUS_DOT: Record<DirectiveStatus, string> = { archived: "bg-[#3a4a6a]", }; -// Per-task dot color for the sidebar entries inside a directive folder. -// Matches the StepsBlockNode palette. -const STEP_STATUS_DOT: Record<string, string> = { - pending: "bg-[#556677]", - ready: "bg-[#9bc3ff]", - running: "bg-yellow-400", - done: "bg-green-400", - failed: "bg-red-400", - skipped: "bg-[#3a4a6a]", +// Per-document status palette. Active/draft documents use the same bright +// green-ish accent as a running directive; shipped/archived use a muted blue. +const DOC_STATUS_DOT: Record<DirectiveDocumentStatus, string> = { + draft: "bg-[#556677]", + active: "bg-green-400", + shipped: "bg-[#75aafc]", + archived: "bg-[#3a4a6a]", +}; + +// ============================================================================= +// Sidebar grouping — group directives by lifecycle stage so the file tree +// reads like a folder per status. We collapse the noisy ones (Archived) by +// default and keep Active / Idle expanded. +// ============================================================================= + +type SidebarGroup = "active" | "idle" | "archived"; + +const GROUP_LABEL: Record<SidebarGroup, string> = { + active: "active", + idle: "idle", + archived: "archived", }; +function bucketOf(status: DirectiveStatus): SidebarGroup { + if (status === "active" || status === "paused") return "active"; + if (status === "archived") return "archived"; + // draft + idle land in the idle bucket (i.e. "not currently running"). + return "idle"; +} + +// Slugify a document title for the displayed `.md` filename, falling back to +// the directive title and finally the document id slice when the title is +// empty. Mirrors the file-naming fix from step 1 (use the user-readable label +// rather than just an id slice). Accepts either a DirectiveSummary or a full +// DirectiveWithSteps — only `title` is read. +function fileLabel( + doc: DirectiveDocument, + directive: { title: string }, +): string { + const docTitle = doc.title.trim(); + if (docTitle.length > 0) return docTitle; + const dirTitle = directive.title.trim(); + if (dirTitle.length > 0) return dirTitle; + return doc.id.slice(0, 8); +} + // ============================================================================= // Sidebar icons (inline SVG, no new deps) // ============================================================================= @@ -112,103 +131,6 @@ function FileIcon() { ); } -/** Terminal/prompt icon for orchestrator and step tasks. */ -function TaskIcon() { - return ( - <svg - viewBox="0 0 16 16" - width={12} - height={12} - className="shrink-0" - aria-hidden - > - <rect x="1.5" y="3" width="13" height="10" rx="1" fill="none" stroke="#9bc3ff" strokeWidth="1" /> - <path d="M3.5 6l2 2-2 2 M7 10h4" stroke="#9bc3ff" strokeWidth="1" fill="none" strokeLinecap="round" /> - </svg> - ); -} - -/** Asterisk-on-terminal icon for ephemeral spinoff tasks — visually - distinct from the plain TaskIcon used for step-spawned execution tasks - so users can tell at a glance which tasks are part of the DAG vs which - are user-spun side quests. */ -function EphemeralTaskIcon() { - return ( - <svg - viewBox="0 0 16 16" - width={12} - height={12} - className="shrink-0" - aria-hidden - > - <rect x="1.5" y="3" width="13" height="10" rx="1" fill="none" stroke="#c084fc" strokeWidth="1" /> - <path d="M3.5 6l2 2-2 2 M7 10h4" stroke="#c084fc" strokeWidth="1" fill="none" strokeLinecap="round" /> - <path d="M11 4l1 1m-1 0l1-1" stroke="#c084fc" strokeWidth="1" fill="none" /> - </svg> - ); -} - -/** PR-bracket icon for the completion task. */ -function CompletionIcon() { - return ( - <svg - viewBox="0 0 16 16" - width={12} - height={12} - className="shrink-0" - aria-hidden - > - <circle cx="4" cy="4" r="1.4" fill="none" stroke="#9bc3ff" strokeWidth="1" /> - <circle cx="4" cy="12" r="1.4" fill="none" stroke="#9bc3ff" strokeWidth="1" /> - <circle cx="12" cy="12" r="1.4" fill="none" stroke="#9bc3ff" strokeWidth="1" /> - <path d="M4 5.4v5.2 M4 12h6.6 M12 4l0 6.6" stroke="#9bc3ff" strokeWidth="1" fill="none" /> - </svg> - ); -} - -function PinIcon() { - return ( - <svg - viewBox="0 0 16 16" - width={10} - height={10} - className="shrink-0" - aria-hidden - > - <path - d="M8 1.5l1.6 3.6 3.9.4-2.95 2.7.85 3.9L8 10.2 4.6 12.1l.85-3.9L2.5 5.5l3.9-.4z" - fill="#75aafc" - opacity="0.7" - /> - </svg> - ); -} - -/** Tiny chip used for the inline directive-folder hover actions. */ -function FolderActionButton({ - children, - title, - onClick, -}: { - children: React.ReactNode; - title: string; - onClick: () => void; -}) { - return ( - <button - type="button" - title={title} - onClick={(e) => { - e.stopPropagation(); - onClick(); - }} - className="w-5 h-5 flex items-center justify-center text-[10px] text-[#7788aa] hover:text-white hover:bg-[rgba(117,170,252,0.15)] rounded transition-colors" - > - {children} - </button> - ); -} - function Caret({ open }: { open: boolean }) { return ( <svg @@ -224,940 +146,652 @@ function Caret({ open }: { open: boolean }) { } // ============================================================================= -// Sidebar -// ============================================================================= - -// ============================================================================= -// Task row context menu — sits next to DirectiveContextMenu and offers the -// task-level controls (interrupt for orchestrator/completion, complete/fail/ -// skip for step tasks). +// SidebarSelection — exactly one of taskId/documentId is non-null. taskId is +// reserved for a future "task selection" feature (we expose it in the URL +// already so the param shape is stable). documentId picks one of the +// directive's documents. // ============================================================================= -interface TaskContextMenuProps { - x: number; - y: number; - task: FolderTaskRow; - onClose: () => void; - onInterrupt: () => void; - onComplete?: () => void; - onFail?: () => void; - onSkip?: () => void; - /** Send a freeform message to the running task (same wire as the inline comment box). */ - onSendMessage?: () => void; - /** Navigate to the standalone task page for full-screen control. */ - onOpenInTaskPage?: () => void; -} - -function TaskContextMenu({ - x, - y, - task, - onClose, - onInterrupt, - onComplete, - onFail, - onSkip, - onSendMessage, - onOpenInTaskPage, -}: TaskContextMenuProps) { - const ref = useRef<HTMLDivElement>(null); - - useEffect(() => { - const click = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) onClose(); - }; - const key = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - document.addEventListener("mousedown", click); - document.addEventListener("keydown", key); - return () => { - document.removeEventListener("mousedown", click); - document.removeEventListener("keydown", key); - }; - }, [onClose]); - - useEffect(() => { - if (!ref.current) return; - const rect = ref.current.getBoundingClientRect(); - if (rect.right > window.innerWidth) ref.current.style.left = `${x - rect.width}px`; - if (rect.bottom > window.innerHeight) ref.current.style.top = `${y - rect.height}px`; - }, [x, y]); - - const item = - "w-full px-3 py-1.5 text-left text-xs font-mono text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] flex items-center gap-2"; - const divider = "border-t border-[rgba(117,170,252,0.2)] my-1"; - - // Interrupt is meaningful for live tasks (orchestrator-active or running steps). - const showInterrupt = - task.kind === "orchestrator-active" || - task.kind === "completion" || - task.status === "running"; - // Step lifecycle controls only apply to step tasks. - const isStep = task.kind === "step"; - const showComplete = isStep && task.status !== "done"; - const showFail = isStep && task.status !== "failed"; - const showSkip = isStep && task.status !== "skipped"; - - return ( - <div - ref={ref} - className="fixed z-50 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-lg min-w-[180px]" - style={{ left: x, top: y }} - > - <div className="px-3 py-1.5 text-[10px] font-mono text-[#556677] uppercase border-b border-[rgba(117,170,252,0.2)] truncate max-w-[220px]"> - {task.kind === "orchestrator-active" ? "Orchestrator" : task.kind === "completion" ? "Completion" : task.label} - </div> - {showInterrupt && ( - <button - className={item} - onClick={() => { - onInterrupt(); - onClose(); - }} - > - <span className="text-amber-300">⏹</span> - Interrupt - </button> - )} - {(showComplete || showFail || showSkip) && <div className={divider} />} - {showComplete && ( - <button - className={item} - onClick={() => { - onComplete?.(); - onClose(); - }} - > - <span className="text-emerald-400">✓</span> - Mark complete - </button> - )} - {showFail && ( - <button - className={item} - onClick={() => { - onFail?.(); - onClose(); - }} - > - <span className="text-red-400">✗</span> - Mark failed - </button> - )} - {showSkip && ( - <button - className={item} - onClick={() => { - onSkip?.(); - onClose(); - }} - > - <span className="text-[#7788aa]">⤳</span> - Skip - </button> - )} - - {/* Direct task-page actions: send-message and open-in-task-page mirror - what the standalone /exec/:taskId page exposes. */} - {(onSendMessage || onOpenInTaskPage) && <div className={divider} />} - {onSendMessage && ( - <button - className={item} - onClick={() => { - onSendMessage(); - onClose(); - }} - > - <span className="text-cyan-300">⌨</span> - Send message - </button> - )} - {onOpenInTaskPage && ( - <button - className={item} - onClick={() => { - onOpenInTaskPage(); - onClose(); - }} - > - <span className="text-[#75aafc]">↗</span> - Open in task page - </button> - )} - </div> - ); -} - -function slugify(title: string, fallback: string): string { - const slug = title - .trim() - .replace(/\s+/g, "-") - .replace(/[^a-zA-Z0-9._-]/g, "") - .toLowerCase(); - return slug.length > 0 ? slug : fallback; -} - interface SidebarSelection { directiveId: string; - /** null = the directive's document; otherwise a task id (orchestrator/step). */ taskId: string | null; + documentId: string | null; +} + +// ============================================================================= +// Per-directive folder — renders as a collapsible folder containing the +// directive's documents. Loads documents lazily on first open (mirroring the +// pattern from step 1's DirectiveFolder, which fetched the full directive +// only when expanded). +// ============================================================================= + +interface DirectiveFolderProps { + directive: DirectiveSummary; + open: boolean; + onToggle: () => void; + /** Called when the user clicks the folder header itself (after toggle). */ + onHeaderClick: () => void; + selection: SidebarSelection | null; + onSelectDocument: (directiveId: string, doc: DirectiveDocument) => void; + onCreateDocument: (directive: DirectiveSummary) => Promise<void>; + /** + * Document refresh trigger — bumped externally so the folder refetches its + * document list after a create/update happens elsewhere. Primarily used so + * a freshly-created document shows up immediately. + */ + refreshNonce: number; } -/** - * Per-directive folder. Renders the directive as a collapsible folder whose - * body is the pinned document entry (always first) followed by a `tasks/` - * subfolder containing the orchestrator, completion, and step tasks. - * - * Status dot lives on the right side only (single-side, per the v2 design). - * If a directive or task has a pending user question, its icon glows. - */ function DirectiveFolder({ directive, open, onToggle, + onHeaderClick, selection, - onSelect, - pendingTaskIds, - hasPendingForDirective, - onDirectiveContextMenu, - onTaskContextMenu, - onCreateTask, - onQuickAction, -}: { - directive: DirectiveSummary; - open: boolean; - onToggle: () => void; - selection: SidebarSelection | null; - onSelect: (sel: SidebarSelection) => void; - /** Set of task ids that currently have pending user questions. */ - pendingTaskIds: Set<string>; - /** Whether any pending question is associated with this directive. */ - hasPendingForDirective: boolean; - onDirectiveContextMenu: (e: React.MouseEvent, d: DirectiveSummary) => void; - onTaskContextMenu: (e: React.MouseEvent, t: FolderTaskRow, directiveId: string) => void; - /** Open the inline "+ New task" form for this directive. */ - onCreateTask: (d: DirectiveSummary) => void; - /** Trigger a quick action (start/pause/PR) on the directive. */ - onQuickAction: (d: DirectiveSummary, action: "start" | "pause" | "pr") => void; -}) { + onSelectDocument, + onCreateDocument, + refreshNonce, +}: DirectiveFolderProps) { const dotColor = STATUS_DOT[directive.status] ?? STATUS_DOT.draft; - const fileName = `${slugify(directive.title, directive.id.slice(0, 8))}.md`; + const orchestratorRunning = !!directive.orchestratorTaskId; - // Lazy fetch full directive (with steps) only when folder is open. - const { directive: detailed } = useDirective(open ? directive.id : undefined); + // Documents fetched lazily on open. We deliberately scope the fetch to the + // open-state so closed folders don't pay the network cost on initial render. + const [docs, setDocs] = useState<DirectiveDocument[] | null>(null); + const [docsLoading, setDocsLoading] = useState(false); + const [docsError, setDocsError] = useState<string | null>(null); - const docSelected = - selection?.directiveId === directive.id && selection.taskId === null; + // shipped/ subfolder open state — independent of the directive folder. + const [shippedOpen, setShippedOpen] = useState(false); - // Ephemeral tasks attached to this directive (no directive_step_id). Fetched - // lazily when the folder opens; refetched whenever a poll lands on the - // directive's detail (poll-driven freshness). - const [ephemeralTasks, setEphemeralTasks] = useState<TaskSummary[]>([]); - useEffect(() => { - if (!open) return; - let cancelled = false; - listDirectiveEphemeralTasks(directive.id) - .then((res) => { - if (!cancelled) setEphemeralTasks(res.tasks); - }) - .catch((err) => { - // eslint-disable-next-line no-console - console.warn("[makima] failed to load ephemeral tasks", err); - }); - return () => { - cancelled = true; - }; - }, [open, directive.id, directive.updatedAt]); + // Whether a "+ New document" call is in flight (disables the button). + const [creating, setCreating] = useState(false); - // Collect the tasks to surface in the folder body. - const tasks = useMemo( - () => collectTasks(detailed, directive, ephemeralTasks), - [detailed, directive, ephemeralTasks], - ); + const refresh = useCallback(async () => { + setDocsLoading(true); + setDocsError(null); + try { + const list = await listDirectiveDocuments(directive.id); + setDocs(list); + } catch (e) { + setDocsError(e instanceof Error ? e.message : "Failed to load documents"); + } finally { + setDocsLoading(false); + } + }, [directive.id]); - const orchestratorRunning = !!directive.orchestratorTaskId; - // Tasks subfolder open state — independent of the directive folder. - const [tasksOpen, setTasksOpen] = useState<boolean>(true); - // Revisions subfolder — collapsed by default since most contracts have - // no merged history yet. - const [revisionsOpen, setRevisionsOpen] = useState<boolean>(false); - const [revisions, setRevisions] = useState<DirectiveRevision[]>([]); - // Fetch revisions only when the parent folder is open. Re-fetch whenever - // the directive's pr_url changes so a freshly-raised PR appears. + // Fetch on open; refetch when refreshNonce bumps and the folder is open. useEffect(() => { if (!open) return; - let cancelled = false; - listDirectiveRevisions(directive.id) - .then((res) => { - if (!cancelled) setRevisions(res.revisions); - }) - .catch((err) => { - // eslint-disable-next-line no-console - console.warn("[makima] failed to load revisions", err); - }); - return () => { - cancelled = true; - }; - }, [open, directive.id, directive.prUrl]); - - // Inline action buttons on the folder header — visible on hover (and when - // the folder is open) so users don't have to right-click to discover the - // primary directive controls. Mirrors a code-editor sidebar's affordance. - const showStart = - directive.status === "draft" || - directive.status === "paused" || - directive.status === "idle" || - directive.status === "inactive"; - const showPause = directive.status === "active"; + void refresh(); + }, [open, refresh, refreshNonce]); + + // Split the documents into the two visual groups. Memoised so we don't + // recompute on every render. + const { activeDocs, shippedDocs } = useMemo(() => { + const active: DirectiveDocument[] = []; + const shipped: DirectiveDocument[] = []; + for (const d of docs ?? []) { + if (d.status === "shipped" || d.status === "archived") { + shipped.push(d); + } else { + active.push(d); + } + } + // Stable order: by createdAt ascending so the first row is the oldest doc. + active.sort((a, b) => a.createdAt.localeCompare(b.createdAt)); + shipped.sort((a, b) => (b.shippedAt ?? "").localeCompare(a.shippedAt ?? "")); + return { activeDocs: active, shippedDocs: shipped }; + }, [docs]); + + const handleCreate = useCallback(async () => { + if (creating) return; + setCreating(true); + try { + await onCreateDocument(directive); + // Refresh after creating so the new doc appears in the list. + await refresh(); + } finally { + setCreating(false); + } + }, [creating, onCreateDocument, directive, refresh]); + + // Selection helpers — used to highlight the currently-selected doc row. + const selectedDocumentId = + selection && selection.directiveId === directive.id + ? selection.documentId + : null; return ( - <div className="select-none group/dir"> - <div - className="w-full flex items-center gap-1.5 pl-3 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.05)]" - onContextMenu={(e) => onDirectiveContextMenu(e, directive)} + <div className="select-none"> + {/* Directive folder header */} + <button + type="button" + onClick={() => { + onToggle(); + onHeaderClick(); + }} + title={directive.title} + className="w-full flex items-center gap-1.5 pl-9 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)]" > - <button - type="button" - onClick={onToggle} - title={directive.title} - className="flex items-center gap-1.5 flex-1 min-w-0 text-left" - > - <Caret open={open} /> - <FolderIcon open={open} /> - <span className="truncate flex-1">{directive.title}</span> - </button> - - {/* Hover/open-only action chips — discoverable replacement for the - right-click menu. Right-click still works as a power-user fallback. */} - <div - className={`flex items-center gap-0.5 transition-opacity ${ - open - ? "opacity-100" - : "opacity-0 group-hover/dir:opacity-100" - }`} - > - {showStart && ( - <FolderActionButton - title="Start" - onClick={() => onQuickAction(directive, "start")} - > - ▶ - </FolderActionButton> - )} - {showPause && ( - <FolderActionButton - title="Pause" - onClick={() => onQuickAction(directive, "pause")} - > - ❚❚ - </FolderActionButton> - )} - {directive.prUrl && ( - <FolderActionButton - title="Open PR" - onClick={() => - window.open(directive.prUrl ?? "", "_blank", "noreferrer") - } - > - ↗ - </FolderActionButton> - )} - <FolderActionButton - title="New task" - onClick={() => onCreateTask(directive)} - > - + - </FolderActionButton> - </div> - - {/* Status dot — RIGHT side only. Glows when this directive has a - pending user question, or pulses when the orchestrator is live. */} - <StatusDot - color={dotColor} - live={orchestratorRunning} - glow={hasPendingForDirective} - status={directive.status} + <Caret open={open} /> + <FolderIcon open={open} /> + <span + className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dotColor}`} + aria-hidden /> - </div> + <span className="truncate flex-1 text-left"> + {directive.title.trim().length > 0 + ? directive.title + : directive.id.slice(0, 8)} + / + </span> + {orchestratorRunning && ( + <span + className="inline-block w-1.5 h-1.5 rounded-full shrink-0 bg-yellow-400 animate-pulse" + title="Orchestrator running" + aria-label="Orchestrator running" + /> + )} + </button> + {/* Folder body — rendered only when open */} {open && ( - <ul className="py-0.5"> - {/* Pinned document entry — always at the top of the folder. */} - <li> - <button - type="button" - onClick={() => - onSelect({ directiveId: directive.id, taskId: null }) - } - className={`w-full text-left flex items-center gap-1.5 pl-8 pr-3 py-1 font-mono text-[11px] transition-colors ${ - docSelected - ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]" - : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent" - }`} - > - <PinIcon /> - <FileIcon /> - <span className="truncate flex-1">{fileName}</span> - </button> - </li> - - {/* tasks/ subfolder — collapsible, contains orchestrator/completion/steps. */} - <li> - <button - type="button" - onClick={() => setTasksOpen((p) => !p)} - className="w-full flex items-center gap-1.5 pl-8 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.05)]" - > - <Caret open={tasksOpen} /> - <FolderIcon open={tasksOpen} /> - <span className="truncate flex-1 text-left">tasks/</span> - {tasks.length > 0 && ( - <span className="text-[10px] text-[#556677]">{tasks.length}</span> - )} - </button> - - {tasksOpen && ( - <ul className="py-0.5"> - {tasks.length === 0 ? ( - <li className="pl-14 pr-3 py-1 font-mono text-[10px] text-[#556677]"> - No tasks yet - </li> - ) : ( - tasks.map((t) => { - const isSelected = - selection?.directiveId === directive.id && - selection?.taskId === t.taskId; - const tdot = STEP_STATUS_DOT[t.status] ?? STEP_STATUS_DOT.pending; - const live = - t.status === "running" || t.kind === "orchestrator-active"; - const glow = pendingTaskIds.has(t.taskId); - const Icon = - t.kind === "completion" - ? CompletionIcon - : t.kind === "ephemeral" - ? EphemeralTaskIcon - : TaskIcon; - return ( - <li key={t.taskId}> - <button - type="button" - onClick={() => - onSelect({ - directiveId: directive.id, - taskId: t.taskId, - }) - } - onContextMenu={(e) => - onTaskContextMenu(e, t, directive.id) - } - title={t.label} - className={`w-full text-left flex items-center gap-1.5 pl-14 pr-3 py-1 font-mono text-[11px] transition-colors ${ - isSelected - ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]" - : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent" - }`} - > - <Icon /> - <span className="truncate flex-1">{t.label}</span> - <StatusDot - color={tdot} - live={live} - glow={glow} - status={t.status} - /> - </button> - </li> - ); - }) - )} - </ul> - )} - </li> + <div className="py-0.5"> + {docsLoading && !docs && ( + <div className="pl-14 pr-3 py-1 font-mono text-[10px] text-[#556677]"> + Loading documents… + </div> + )} + {docsError && ( + <div className="pl-14 pr-3 py-1 font-mono text-[10px] text-red-400"> + {docsError} + </div> + )} - {/* revisions/ subfolder — per-PR frozen snapshots of this contract. - Only rendered when there's at least one revision; otherwise the - folder body would be a confusing empty placeholder. */} - {revisions.length > 0 && ( - <li> + {/* Active group */} + {docs && ( + <> + {/* + New document affordance — sits at the top of the active list + so the user can always reach it without scrolling past + existing docs. */} <button type="button" - onClick={() => setRevisionsOpen((p) => !p)} - className="w-full flex items-center gap-1.5 pl-8 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.05)]" + onClick={handleCreate} + disabled={creating} + className="w-full flex items-center gap-1.5 pl-14 pr-3 py-1 font-mono text-[11px] text-emerald-400 hover:bg-[rgba(74,222,128,0.06)] disabled:opacity-50" + title="Create a new document under this directive" > - <Caret open={revisionsOpen} /> - <FolderIcon open={revisionsOpen} /> - <span className="truncate flex-1 text-left">revisions/</span> - <span className="text-[10px] text-[#556677]"> - {revisions.length} - </span> + <span className="text-[12px] leading-none">+</span> + <span>New document</span> </button> - {revisionsOpen && ( - <ul className="py-0.5"> - {revisions.map((r) => { - const isSelected = - selection?.directiveId === directive.id && - selection?.taskId === `revision:${r.id}`; - return ( - <li key={r.id}> - <button - type="button" - onClick={() => - onSelect({ - directiveId: directive.id, - taskId: `revision:${r.id}`, - }) - } - title={`v${r.version} · ${r.prState} · ${r.prUrl}`} - className={`w-full text-left flex items-center gap-1.5 pl-14 pr-3 py-1 font-mono text-[11px] transition-colors ${ - isSelected - ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]" - : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent" - }`} - > - <FileIcon /> - <span className="truncate flex-1"> - v{r.version}.md - </span> - <RevisionStateBadge prState={r.prState} /> - </button> - </li> - ); - })} - </ul> + {activeDocs.length === 0 && !docsLoading && ( + <div className="pl-14 pr-3 py-1 font-mono text-[10px] text-[#556677] italic"> + no active documents + </div> + )} + + {activeDocs.map((doc) => ( + // Each active document gets its own tasks/ subfolder + // immediately below it. Active docs default-open the + // folder so the user sees their live work without an + // extra click. + <div key={doc.id}> + <DocumentRow + doc={doc} + directive={directive} + selected={doc.id === selectedDocumentId} + onSelect={() => onSelectDocument(directive.id, doc)} + /> + <DocumentTasksFolder + documentId={doc.id} + depth="normal" + defaultOpen={doc.status === "active"} + refreshNonce={refreshNonce} + /> + </div> + ))} + + {/* shipped/ subfolder — only rendered when at least one shipped + or archived doc exists. Hidden entirely otherwise so empty + directives stay tidy. */} + {shippedDocs.length > 0 && ( + <div> + <button + type="button" + onClick={() => setShippedOpen((v) => !v)} + className="w-full flex items-center gap-1.5 pl-14 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)]" + > + <Caret open={shippedOpen} /> + <FolderIcon open={shippedOpen} /> + <span>shipped/</span> + <span className="ml-auto text-[10px] text-[#556677]"> + {shippedDocs.length} + </span> + </button> + {shippedOpen && + shippedDocs.map((doc) => ( + // Shipped docs render the doc row + its frozen + // tasks/ subfolder. The tasks/ folder defaults + // closed (history) so it doesn't dominate the + // sidebar; users can click to inspect what work + // produced this shipped contract. + <div key={doc.id}> + <DocumentRow + doc={doc} + directive={directive} + selected={doc.id === selectedDocumentId} + onSelect={() => onSelectDocument(directive.id, doc)} + indent="deep" + /> + <DocumentTasksFolder + documentId={doc.id} + depth="deep" + defaultOpen={false} + refreshNonce={refreshNonce} + /> + </div> + ))} + </div> )} - </li> + </> )} - </ul> + </div> )} </div> ); } -/** - * Read-only viewer for a frozen contract revision. We render the markdown as - * plain pre-formatted text — these are immutable historical records, not - * places to edit. A header strip shows the PR state and a deep link. - */ -function RevisionViewer({ - directiveId, - revisionId, -}: { - directiveId: string; - revisionId: string; -}) { - const [revision, setRevision] = useState<DirectiveRevision | null>(null); - const [loading, setLoading] = useState(true); +// ============================================================================= +// DocumentRow — one row inside a directive folder. The indent depth differs +// between active rows (one level deep) and shipped rows (two levels deep). +// ============================================================================= + +interface DocumentRowProps { + doc: DirectiveDocument; + directive: DirectiveSummary; + selected: boolean; + onSelect: () => void; + indent?: "normal" | "deep"; +} + +function DocumentRow({ + doc, + directive, + selected, + onSelect, + indent = "normal", +}: DocumentRowProps) { + const dot = DOC_STATUS_DOT[doc.status] ?? DOC_STATUS_DOT.draft; + const padLeft = indent === "deep" ? "pl-[88px]" : "pl-14"; + const name = `${fileLabel(doc, directive)}.md`; + + return ( + <button + type="button" + onClick={onSelect} + title={name} + className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] transition-colors ${ + selected + ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]" + : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent" + }`} + > + <FileIcon /> + <span + className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} + aria-hidden + title={doc.status} + /> + <span className="truncate flex-1">{name}</span> + {/* Status chip — only shown for non-active states so the row stays + uncluttered for the common case. */} + {doc.status !== "active" && ( + <span className="text-[9px] uppercase tracking-wide text-[#556677]"> + {doc.status} + </span> + )} + {/* PR badge for shipped docs. The link short-circuits the row's + onClick so clicking the PR doesn't also re-select the doc. */} + {doc.prUrl && (doc.status === "shipped" || doc.status === "archived") && ( + <a + href={doc.prUrl} + target="_blank" + rel="noreferrer noopener" + onClick={(e) => e.stopPropagation()} + className="text-[9px] text-[#75aafc] hover:text-white border border-[#2a3a5a] rounded px-1" + title={doc.prUrl} + > + PR + </a> + )} + </button> + ); +} + +// ============================================================================= +// Per-document tasks/ subfolder — fetches the steps + ephemeral tasks for a +// single document and renders a collapsible `tasks/` row beneath the +// document. Lazy: fetch only fires once the user opens the folder, and we +// also keep the folder closed by default for shipped docs (where it's +// historical) and open by default for active docs (where it's live work). +// ============================================================================= + +interface DocumentTasksFolderProps { + documentId: string; + /** Visual indent depth — mirrors the parent DocumentRow's indent so the + * tasks/ row sits one level deeper than its parent doc. */ + depth: "normal" | "deep"; + /** Whether to fetch+open by default. Active docs default to open so the + * user sees their live tasks immediately; shipped docs default to closed + * (historical), and the user can click to expand. */ + defaultOpen: boolean; + /** Bumped externally so the folder refetches its task list after a save + * or status change elsewhere. Same nonce used for the directive folder. */ + refreshNonce: number; +} + +function DocumentTasksFolder({ + documentId, + depth, + defaultOpen, + refreshNonce, +}: DocumentTasksFolderProps) { + const [open, setOpen] = useState(defaultOpen); + const [data, setData] = useState<DocumentTasksResponse | null>(null); + const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); - useEffect(() => { - let cancelled = false; + // Inner row indent is one level deeper than the folder header. Folder + // header uses pl-[88px] (deep) or pl-14 (normal); tasks rows go one + // step beyond that. + const headerPadLeft = depth === "deep" ? "pl-[88px]" : "pl-14"; + const rowPadLeft = depth === "deep" ? "pl-[112px]" : "pl-[72px]"; + + const refresh = useCallback(async () => { setLoading(true); setError(null); - listDirectiveRevisions(directiveId) - .then((res) => { - if (cancelled) return; - const found = res.revisions.find((r) => r.id === revisionId) ?? null; - if (!found) setError("Revision not found"); - setRevision(found); - }) - .catch((err) => { - if (cancelled) return; - setError(err instanceof Error ? err.message : String(err)); - }) - .finally(() => { - if (!cancelled) setLoading(false); - }); - return () => { - cancelled = true; - }; - }, [directiveId, revisionId]); + try { + const res = await listDirectiveDocumentTasks(documentId); + setData(res); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load tasks"); + } finally { + setLoading(false); + } + }, [documentId]); - if (loading) { - return ( - <div className="flex-1 flex items-center justify-center"> - <p className="text-[#556677] font-mono text-[12px]">Loading revision…</p> - </div> - ); - } - if (error || !revision) { - return ( - <div className="flex-1 flex items-center justify-center"> - <p className="text-red-400 font-mono text-[12px]"> - {error ?? "Revision not found"} - </p> - </div> - ); + // Fetch when the folder is open (initial open or refresh). We don't + // pre-fetch on closed folders so we don't waste bandwidth on the long + // tail of historical shipped docs the user never expands. + useEffect(() => { + if (!open) return; + void refresh(); + }, [open, refresh, refreshNonce]); + + const total = (data?.steps.length ?? 0) + (data?.tasks.length ?? 0); + + // Don't render the folder at all if we've fetched and the document has + // no tasks. This is the cleanest visual: a draft document just shows up + // as a single row with no children. The empty-folder check is gated on + // a successful fetch so we don't flash "no tasks/" rows during loading. + if (data && total === 0 && !loading && !error) { + return null; } return ( - <div className="flex-1 flex flex-col h-full overflow-hidden bg-[#0a1628]"> - <div className="flex-1 overflow-y-auto"> - <div className="max-w-3xl mx-auto px-8 py-10 text-[#dbe7ff]"> - <div className="flex items-center gap-3 mb-1"> - <h1 className="text-[24px] font-medium text-white tracking-tight"> - v{revision.version} - </h1> - <RevisionStateBadge prState={revision.prState} /> - </div> - <p className="text-[10px] font-mono text-[#556677] uppercase tracking-wide mb-1"> - Frozen {new Date(revision.frozenAt).toLocaleString()} - </p> - <p className="text-[11px] font-mono text-[#7788aa] mb-8"> - <a - href={revision.prUrl} - target="_blank" - rel="noreferrer" - className="text-[#75aafc] hover:text-[#9bc3ff] underline" - > - {revision.prUrl} - </a> - </p> - - {/* Render the frozen markdown as plain pre-formatted text. We - deliberately do not parse it into rich nodes — the goal is to - show the historical record exactly as it was at PR time. */} - <pre className="whitespace-pre-wrap break-words font-mono text-[13px] leading-relaxed text-[#e0eaf8]"> - {revision.content} - </pre> + <div> + <button + type="button" + onClick={() => setOpen((v) => !v)} + className={`w-full flex items-center gap-1.5 ${headerPadLeft} pr-3 py-1 font-mono text-[11px] text-[#7788aa] hover:bg-[rgba(117,170,252,0.06)]`} + > + <Caret open={open} /> + <FolderIcon open={open} /> + <span>tasks/</span> + {total > 0 && ( + <span className="ml-auto text-[10px] text-[#556677]">{total}</span> + )} + </button> + {open && ( + <div className="py-0.5"> + {loading && !data && ( + <div className={`${rowPadLeft} pr-3 py-1 font-mono text-[10px] text-[#556677]`}> + Loading tasks… + </div> + )} + {error && ( + <div className={`${rowPadLeft} pr-3 py-1 font-mono text-[10px] text-red-400`}> + {error} + </div> + )} + {data?.steps.map((step) => ( + <StepRow key={`step-${step.id}`} step={step} padLeft={rowPadLeft} /> + ))} + {data?.tasks.map((task) => ( + <TaskRow key={`task-${task.id}`} task={task} padLeft={rowPadLeft} /> + ))} </div> - </div> + )} </div> ); } -/** Tiny pill showing the PR state of a revision (open / merged / closed). */ -function RevisionStateBadge({ prState }: { prState: string }) { - const tone = - prState === "merged" - ? "text-emerald-300 border-emerald-700/60" - : prState === "closed" - ? "text-[#7788aa] border-[#2a3a5a]" - : "text-amber-300 border-amber-600/40"; - return ( - <span - className={`text-[9px] font-mono uppercase border rounded px-1 py-0 ${tone}`} - > - {prState} - </span> - ); +// Step status → coloured dot, mirroring directive status palette so the +// sidebar reads consistently. +const STEP_STATUS_DOT: Record<string, string> = { + pending: "bg-[#556677]", + ready: "bg-[#9bc3ff]", + running: "bg-yellow-400", + completed: "bg-green-400", + failed: "bg-red-400", + skipped: "bg-[#3a4a6a]", +}; + +// Task status → coloured dot. Statuses come from the Task model; the small +// set we expect in directive context is enough — anything else falls back +// to the muted "draft" colour. +const TASK_STATUS_DOT: Record<string, string> = { + pending: "bg-[#556677]", + starting: "bg-yellow-400", + running: "bg-yellow-400", + completed: "bg-green-400", + failed: "bg-red-400", + cancelled: "bg-[#3a4a6a]", + interrupted: "bg-orange-400", +}; + +interface StepRowProps { + step: DirectiveStep; + padLeft: string; } -/** - * Right-side status indicator. Composes the colored status dot with optional - * "live" pulse (orchestrator running) and "glow" attention ring (pending user - * question waiting on a response). - */ -function StatusDot({ - color, - live, - glow, - status, -}: { - color: string; - live: boolean; - glow: boolean; - status: string; -}) { - // The glow is a soft amber ring pulsed via box-shadow. Keep it subtle so it - // doesn't fight the live pulse for attention when both are present. - const ring = glow - ? "shadow-[0_0_0_2px_rgba(251,191,36,0.45),0_0_8px_2px_rgba(251,191,36,0.55)] animate-pulse" - : ""; - const livePulse = live && !glow ? "animate-pulse" : ""; - const title = glow - ? `${status} — needs response` - : live - ? `${status} — running` - : `status: ${status}`; +function StepRow({ step, padLeft }: StepRowProps) { + const dot = STEP_STATUS_DOT[step.status] ?? "bg-[#556677]"; return ( - <span - className={`inline-block w-2 h-2 rounded-full shrink-0 ${color} ${ring} ${livePulse}`} - aria-label={title} - title={title} - /> + <div + title={`${step.name} (${step.status})`} + className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] text-[#9bc3ff]`} + > + <FileIcon /> + <span + className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} + aria-hidden + title={step.status} + /> + <span className="truncate flex-1">{step.name}</span> + <span className="text-[9px] uppercase tracking-wide text-[#556677]"> + step + </span> + </div> ); } -interface FolderTaskRow { - taskId: string; - /** Directive step id for step kinds — needed for complete/fail/skip APIs. */ - stepId: string | null; - label: string; - status: string; - kind: "orchestrator-active" | "completion" | "step" | "ephemeral"; +interface TaskRowProps { + task: Task; + padLeft: string; } -function collectTasks( - detailed: DirectiveWithSteps | null, - summary: DirectiveSummary, - ephemeralTasks: TaskSummary[], -): FolderTaskRow[] { - const rows: FolderTaskRow[] = []; - - // Orchestrator (planner) — surfaces only while it's actively running so - // the folder is not flooded with stale orchestrator entries. - const orchestratorId = - detailed?.orchestratorTaskId ?? summary.orchestratorTaskId ?? null; - if (orchestratorId) { - rows.push({ - taskId: orchestratorId, - stepId: null, - label: "orchestrator", - status: "running", - kind: "orchestrator-active", - }); - } - - // Completion (PR creation) task. - const completionId = - detailed?.completionTaskId ?? summary.completionTaskId ?? null; - if (completionId) { - rows.push({ - taskId: completionId, - stepId: null, - label: "completion", - status: "running", - kind: "completion", - }); - } - - // Step tasks — only steps that have actually been started have a taskId. - if (detailed) { - for (const step of detailed.steps) { - if (!step.taskId) continue; - rows.push({ - taskId: step.taskId, - stepId: step.id, - label: step.name, - status: step.status, - kind: "step", - }); - } - } - - // Ephemeral tasks — user-spawned spinoffs not part of the DAG. Surfaced - // alongside step tasks but with a different icon and the "ephemeral" kind - // so context menus and the merge button behave correctly. - for (const t of ephemeralTasks) { - rows.push({ - taskId: t.id, - stepId: null, - label: t.name, - status: t.status, - kind: "ephemeral", - }); - } - - return rows; +function TaskRow({ task, padLeft }: TaskRowProps) { + const dot = TASK_STATUS_DOT[task.status] ?? "bg-[#556677]"; + // Supervisor tasks get a small "sup" tag so the user can spot + // contract orchestrators in the list. + const isSup = task.isSupervisor; + return ( + <div + title={`${task.name} (${task.status})`} + className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] text-[#9bc3ff]`} + > + <FileIcon /> + <span + className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} + aria-hidden + title={task.status} + /> + <span className="truncate flex-1">{task.name}</span> + <span className="text-[9px] uppercase tracking-wide text-[#556677]"> + {isSup ? "sup" : "task"} + </span> + </div> + ); } +// ============================================================================= +// Sidebar +// ============================================================================= + interface SidebarProps { directives: DirectiveSummary[]; loading: boolean; selection: SidebarSelection | null; - onSelect: (sel: SidebarSelection) => void; - onDirectiveContextMenu: (e: React.MouseEvent, d: DirectiveSummary) => void; - onTaskContextMenu: (e: React.MouseEvent, t: FolderTaskRow, directiveId: string) => void; - /** Open the inline "+ New task" form for this directive. */ - onCreateTask: (d: DirectiveSummary) => void; - /** Trigger a quick action (start/pause/PR) on the directive. */ - onQuickAction: (d: DirectiveSummary, action: "start" | "pause" | "pr") => void; - /** Navigate to an orphan (no-directive) task's standalone view. */ - onSelectOrphan: (taskId: string) => void; + onSelectDocument: (directiveId: string, doc: DirectiveDocument) => void; + onSelectDirective: (directiveId: string) => void; + onCreateDocument: (directive: DirectiveSummary) => Promise<void>; + refreshNonce: number; } function DocumentSidebar({ directives, loading, selection, - onSelect, - onDirectiveContextMenu, - onTaskContextMenu, - onCreateTask, - onQuickAction, - onSelectOrphan, + onSelectDocument, + onSelectDirective, + onCreateDocument, + refreshNonce, }: SidebarProps) { - // Orphan tasks (no directive) — top-level "tmp/" pseudo-folder. Polled - // every 5s so newly-spawned standalone tasks appear without a manual - // refresh. - const [orphanTasks, setOrphanTasks] = useState<TaskSummary[]>([]); - useEffect(() => { - let cancelled = false; - const load = () => { - listOrphanTasks() - .then((res) => { - if (!cancelled) setOrphanTasks(res.tasks); - }) - .catch(() => { - /* swallow — tmp/ is a nice-to-have, never blocking */ - }); + const groups: Record<SidebarGroup, DirectiveSummary[]> = useMemo(() => { + const out: Record<SidebarGroup, DirectiveSummary[]> = { + active: [], + idle: [], + archived: [], }; - load(); - const interval = setInterval(load, 5000); - return () => { - cancelled = true; - clearInterval(interval); - }; - }, []); - const [tmpOpen, setTmpOpen] = useState<boolean>(true); - // Pending user questions — drives the "glow" attention ring. We split into - // two indices so the directive folder header glows whenever ANY of its - // tasks has a pending question, while individual task rows glow only for - // their own question. - const { pendingQuestions } = useSupervisorQuestions(); - const { directivesWithPending, tasksWithPending } = useMemo(() => { - const dirs = new Set<string>(); - const tasks = new Set<string>(); - for (const q of pendingQuestions) { - if (q.directiveId) dirs.add(q.directiveId); - if (q.taskId) tasks.add(q.taskId); + for (const d of directives) { + out[bucketOf(d.status)].push(d); } - return { directivesWithPending: dirs, tasksWithPending: tasks }; - }, [pendingQuestions]); - - // Sort active first, then idle, then paused, then drafts, then inactive - // (shipped contracts are quieter), then archived. - const sorted = useMemo(() => { - const order: Record<DirectiveStatus, number> = { - active: 0, - paused: 1, - idle: 2, - draft: 3, - inactive: 4, - archived: 5, - }; - return [...directives].sort((a, b) => { - const oa = order[a.status] ?? 99; - const ob = order[b.status] ?? 99; - if (oa !== ob) return oa - ob; - return a.title.localeCompare(b.title, undefined, { sensitivity: "base" }); + // Sort each group alphabetically so it feels like a stable file tree. + (Object.keys(out) as SidebarGroup[]).forEach((k) => { + out[k].sort((a, b) => + a.title.localeCompare(b.title, undefined, { sensitivity: "base" }), + ); }); + return out; }, [directives]); - // Track which directive folders are open. The currently selected directive - // is forced open so deep links land on something visible. - const [openIds, setOpenIds] = useState<Set<string>>(new Set()); - const lastSelectedRef = useRef<string | null>(null); + // Default-collapsed state per group folder. + const [openGroups, setOpenGroups] = useState<Record<SidebarGroup, boolean>>({ + active: true, + idle: true, + archived: false, + }); + + // Per-directive open state. We auto-open the directive containing the + // current selection so the user can see what they're editing. + const [openDirectives, setOpenDirectives] = useState<Record<string, boolean>>({}); + useEffect(() => { - if (selection && selection.directiveId !== lastSelectedRef.current) { - lastSelectedRef.current = selection.directiveId; - setOpenIds((prev) => { - if (prev.has(selection.directiveId)) return prev; - const next = new Set(prev); - next.add(selection.directiveId); - return next; - }); - } - }, [selection]); - - const toggleOpen = useCallback((id: string) => { - setOpenIds((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }, []); + if (!selection) return; + setOpenDirectives((prev) => + prev[selection.directiveId] ? prev : { ...prev, [selection.directiveId]: true }, + ); + }, [selection?.directiveId]); + + const toggleGroup = (g: SidebarGroup) => + setOpenGroups((prev) => ({ ...prev, [g]: !prev[g] })); + + const toggleDirective = (id: string) => + setOpenDirectives((prev) => ({ ...prev, [id]: !prev[id] })); return ( <div className="flex flex-col h-full"> {/* Sidebar header */} <div className="flex items-center justify-between px-3 py-2 border-b border-dashed border-[rgba(117,170,252,0.2)]"> <span className="text-[11px] font-mono text-[#9bc3ff] uppercase tracking-wide"> - Contracts + Documents </span> <span className="text-[10px] font-mono text-[#556677]"> {directives.length} </span> </div> - {/* Top-level "contracts/" folder header (informational, non-interactive). */} + {/* Top-level "directives/" folder */} <div className="flex items-center gap-1.5 px-3 py-1.5 text-[11px] font-mono text-[#9bc3ff]"> <FolderIcon open /> - <span>contracts/</span> + <span>directives/</span> </div> {/* Body */} <div className="flex-1 overflow-y-auto pb-4"> - {/* tmp/ pseudo-folder — orphan tasks (directive_id NULL). Always - rendered so users can create scratchpad tasks even when zero - directives exist; collapses to a thin header when empty. */} - <div className="select-none border-b border-dashed border-[rgba(117,170,252,0.1)] pb-1 mb-1"> - <button - type="button" - onClick={() => setTmpOpen((p) => !p)} - className="w-full flex items-center gap-1.5 pl-3 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.05)]" - > - <Caret open={tmpOpen} /> - <FolderIcon open={tmpOpen} /> - <span className="truncate flex-1 text-left text-[#7788aa]">tmp/</span> - <span className="text-[10px] text-[#556677]"> - {orphanTasks.length} - </span> - </button> - {tmpOpen && ( - <ul className="py-0.5"> - {orphanTasks.length === 0 ? ( - <li className="pl-8 pr-3 py-1 font-mono text-[10px] text-[#556677] italic"> - No orphan tasks - </li> - ) : ( - orphanTasks.map((t) => { - const tdot = - STEP_STATUS_DOT[t.status] ?? STEP_STATUS_DOT.pending; - const live = t.status === "running"; - return ( - <li key={t.id}> - <button - type="button" - onClick={() => onSelectOrphan(t.id)} - title={t.name} - className="w-full text-left flex items-center gap-1.5 pl-8 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent transition-colors" - > - <TaskIcon /> - <span className="truncate flex-1">{t.name}</span> - <StatusDot - color={tdot} - live={live} - glow={false} - status={t.status} - /> - </button> - </li> - ); - }) - )} - </ul> - )} - </div> - {loading && directives.length === 0 ? ( <div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]"> Loading... </div> ) : directives.length === 0 ? ( <div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]"> - No contracts yet + No directives yet </div> ) : ( - sorted.map((d) => ( - <DirectiveFolder - key={d.id} - directive={d} - open={openIds.has(d.id)} - onToggle={() => toggleOpen(d.id)} - selection={selection} - onSelect={onSelect} - pendingTaskIds={tasksWithPending} - hasPendingForDirective={directivesWithPending.has(d.id)} - onDirectiveContextMenu={onDirectiveContextMenu} - onTaskContextMenu={onTaskContextMenu} - onCreateTask={onCreateTask} - onQuickAction={onQuickAction} - /> - )) + (Object.keys(groups) as SidebarGroup[]).map((group) => { + const list = groups[group]; + if (list.length === 0) return null; + const open = openGroups[group]; + return ( + <div key={group} className="select-none"> + {/* Group header (sub-folder) */} + <button + type="button" + onClick={() => toggleGroup(group)} + className="w-full flex items-center gap-1.5 pl-4 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.05)]" + > + <Caret open={open} /> + <FolderIcon open={open} /> + <span>{GROUP_LABEL[group]}/</span> + <span className="ml-auto text-[10px] text-[#556677]"> + {list.length} + </span> + </button> + + {/* Each directive is a folder containing N documents. */} + {open && ( + <div className="py-0.5"> + {list.map((d) => ( + <DirectiveFolder + key={d.id} + directive={d} + open={!!openDirectives[d.id]} + onToggle={() => toggleDirective(d.id)} + onHeaderClick={() => onSelectDirective(d.id)} + selection={selection} + onSelectDocument={onSelectDocument} + onCreateDocument={onCreateDocument} + refreshNonce={refreshNonce} + /> + ))} + </div> + )} + </div> + ); + }) )} </div> </div> @@ -1166,108 +800,112 @@ function DocumentSidebar({ // ============================================================================= // Editor shell — wraps DocumentEditor and handles the "no document selected" -// and loading states. +// and loading states. Two modes: +// 1) documentId selected → fetch the DirectiveDocument and edit doc.body via +// updateDirectiveDocument (the call that auto-reactivates a shipped doc). +// 2) no documentId (legacy fallback, kept for the "select a directive but +// not a document" transitional case) → edit directive.goal as before. // ============================================================================= -/** - * Wraps DocumentTaskStream with ephemeral-aware metadata. Determines whether - * the selected task is part of the directive's DAG (orchestrator/completion/ - * steps) or an ephemeral spinoff, and looks up its current status from the - * ephemeral list — that decides whether the "Merge to base" affordance - * should appear in the stream's action header. - */ -function EphemeralAwareTaskStream({ - taskId, - label, - directive, -}: { - taskId: string; - label: string; - directive: DirectiveWithSteps; -}) { - const isStepBound = - taskId === directive.orchestratorTaskId || - taskId === directive.completionTaskId || - directive.steps.some((s) => s.taskId === taskId); - - // Status lookup for ephemeral tasks. We poll the ephemeral list lazily — - // this is a lightweight call and only triggers when the user is viewing a - // task in the editor pane. - const [ephemeralStatus, setEphemeralStatus] = useState<string | undefined>(); - useEffect(() => { - if (isStepBound) return; - let cancelled = false; - const load = () => { - listDirectiveEphemeralTasks(directive.id) - .then((res) => { - if (cancelled) return; - const match = res.tasks.find((t) => t.id === taskId); - setEphemeralStatus(match?.status); - }) - .catch(() => { - /* non-blocking */ - }); - }; - load(); - const interval = setInterval(load, 5000); - return () => { - cancelled = true; - clearInterval(interval); - }; - }, [taskId, directive.id, isStepBound]); - - return ( - <DocumentTaskStream - taskId={taskId} - label={label} - ephemeral={!isStepBound} - status={ephemeralStatus} - /> - ); -} - interface EditorShellProps { - selectedId: string | undefined; - selectedTaskId: string | null; + selection: SidebarSelection | null; hasDirectives: boolean; listLoading: boolean; - onClearTask: () => void; + /** Bumped after a successful document save so the sidebar refetches. */ + onDocumentChanged: () => void; } function EditorShell({ - selectedId, - selectedTaskId, + selection, hasDirectives, listLoading, - onClearTask, + onDocumentChanged, }: EditorShellProps) { + const directiveId = selection?.directiveId; + const documentId = selection?.documentId ?? null; + + // We deliberately don't pull `updateGoal` here — in the multi-document + // world, edits flow through updateDirectiveDocument (which auto-reactivates + // a shipped doc when its body changes). The legacy directive.goal is + // unused on this surface. const { directive, - loading, - updateGoal, + loading: directiveLoading, cleanup, createPR, pickUpOrders, - } = useDirective(selectedId); + } = useDirective(directiveId); + + // Document fetch — only when documentId is selected. Refetched whenever the + // id changes; not polled (the document stream is too low-traffic to warrant + // background refresh in this iteration). + const [doc, setDoc] = useState<DirectiveDocument | null>(null); + const [docLoading, setDocLoading] = useState(false); + const [docError, setDocError] = useState<string | null>(null); + + useEffect(() => { + if (!documentId) { + setDoc(null); + setDocLoading(false); + setDocError(null); + return; + } + let cancelled = false; + setDocLoading(true); + setDocError(null); + getDirectiveDocument(documentId) + .then((d) => { + if (cancelled) return; + setDoc(d); + }) + .catch((e) => { + if (cancelled) return; + setDocError(e instanceof Error ? e.message : "Failed to load document"); + }) + .finally(() => { + if (cancelled) return; + setDocLoading(false); + }); + return () => { + cancelled = true; + }; + }, [documentId]); + + // Save callback for the document path. The backend re-stamps a shipped doc + // back to active when its body changes, so we just optimistically update + // local state with the server's response. + const onUpdateDocumentBody = useCallback( + async (body: string) => { + if (!documentId) return; + const updated = await updateDirectiveDocument(documentId, { body }); + setDoc(updated); + // Tell the sidebar to refetch the directive's document list so the + // status chip flips from `shipped` back to `active` (and any title + // changes propagate). Cheap — folders only refetch when open. + onDocumentChanged(); + }, + [documentId, onDocumentChanged], + ); - if (!selectedId) { + // ---- Empty / error / loading states ------------------------------------ + if (!directiveId) { return ( <div className="flex-1 flex items-center justify-center h-full"> <p className="text-[#556677] font-mono text-[12px]"> {listLoading - ? "Loading contracts..." + ? "Loading documents..." : hasDirectives - ? "Select a contract from the sidebar" - : "No contracts yet — create one from the legacy UI"} + ? "Select a document from the sidebar" + : "No directives yet — create one from the legacy UI"} </p> </div> ); } - if (loading && !directive) { + if (directiveLoading && !directive) { return ( <div className="flex-1 flex items-center justify-center h-full"> - <p className="text-[#556677] font-mono text-[12px]">Loading contract...</p> + <p className="text-[#556677] font-mono text-[12px]">Loading directive...</p> </div> ); } @@ -1275,108 +913,86 @@ function EditorShell({ if (!directive) { return ( <div className="flex-1 flex items-center justify-center h-full"> - <p className="text-[#7788aa] font-mono text-[12px]">Contract not found</p> + <p className="text-[#7788aa] font-mono text-[12px]">Directive not found</p> </div> ); } - // The "task" param can encode either a real task id, or a revision via the - // `revision:<uuid>` prefix. Split that out so the right pane can switch - // between the live task stream and the read-only revision viewer. - const revisionId = - selectedTaskId && selectedTaskId.startsWith("revision:") - ? selectedTaskId.slice("revision:".length) - : null; - const realTaskId = revisionId ? null : selectedTaskId; - - // Resolve the label for the breadcrumb when a task is selected. - const taskLabel = realTaskId - ? realTaskId === directive.orchestratorTaskId - ? "orchestrator" - : realTaskId === directive.completionTaskId - ? "completion" - : directive.steps.find((s) => s.taskId === realTaskId)?.name ?? - realTaskId.slice(0, 8) - : revisionId - ? "revision" - : null; - - // "Now executing" strip — surfaces what's live when looking at the - // contract editor, so users don't have to scan the sidebar to find it. - const liveTask = (() => { - if (selectedTaskId) return null; // already viewing a task; strip is redundant - if (directive.orchestratorTaskId) { - return { id: directive.orchestratorTaskId, name: "orchestrator" }; + // --- Document path: documentId selected -------------------------------- + if (documentId) { + if (docLoading && !doc) { + return ( + <div className="flex-1 flex items-center justify-center h-full"> + <p className="text-[#556677] font-mono text-[12px]">Loading document...</p> + </div> + ); } - if (directive.completionTaskId) { - return { id: directive.completionTaskId, name: "completion" }; + if (docError) { + return ( + <div className="flex-1 flex items-center justify-center h-full"> + <p className="text-red-400 font-mono text-[12px]">{docError}</p> + </div> + ); } - const runningStep = directive.steps.find((s) => s.status === "running"); - if (runningStep && runningStep.taskId) { - return { id: runningStep.taskId, name: runningStep.name }; + if (!doc) { + return ( + <div className="flex-1 flex items-center justify-center h-full"> + <p className="text-[#7788aa] font-mono text-[12px]">Document not found</p> + </div> + ); } - return null; - })(); - return ( - <div className="flex-1 flex flex-col h-full overflow-hidden"> - {/* Contract header — breadcrumb-like, mirrors a code editor's tab bar */} - <div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]"> - <div className="flex items-center gap-2 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]"> - <FileIcon /> - <span>contracts /</span> - <span className="text-[#9bc3ff]">{directive.id.slice(0, 8)}</span> - {selectedTaskId && ( - <> - <span>/</span> - <span className="text-[#9bc3ff]">{taskLabel}</span> - <button - type="button" - onClick={onClearTask} - className="ml-2 px-1.5 py-0.5 text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded normal-case" - > - back to contract - </button> - </> - )} - </div> - </div> + // Synthesise a directive-shaped object whose `goal` is the document body. + // DocumentEditor was originally written against DirectiveWithSteps, so we + // can keep its shape by overriding `goal` with `doc.body` and `title` + // with the document's filename label. The steps panel still draws from + // the real directive (passed through StepsBlockContextProvider). + const docTitle = `${fileLabel(doc, directive)}.md`; + const directiveAsDocument = { + ...directive, + goal: doc.body, + title: docTitle, + }; - {/* Now-executing strip — only when viewing the contract doc itself. - Click to jump into the live task transcript. */} - {liveTask && ( - <button - type="button" - onClick={() => - // Navigate via the search-param so EditorShell switches to the - // task stream for this live task. - (window.location.search = `?task=${liveTask.id}`) - } - className="shrink-0 flex items-center gap-2 px-6 py-1.5 bg-amber-900/15 border-b border-amber-700/40 text-amber-300 font-mono text-[11px] hover:bg-amber-900/30 transition-colors" - > - <span className="inline-block w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" /> - <span className="uppercase tracking-wide text-[10px]">Now executing</span> - <span className="text-[#dbe7ff]">{liveTask.name}</span> - <span className="ml-auto text-[10px] text-amber-200/70"> - click to view transcript ↗ - </span> - </button> - )} + return ( + <div className="flex-1 flex flex-col h-full overflow-hidden"> + {/* Breadcrumb — directives / <directive title> / <document title>.md */} + <div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]"> + <div className="flex items-center gap-2 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]"> + <FileIcon /> + <span>directives /</span> + <span className="text-[#9bc3ff]"> + {directive.title.trim().length > 0 + ? directive.title + : directive.id.slice(0, 8)} + </span> + <span>/</span> + <span className="text-white">{docTitle}</span> + {doc.status === "shipped" && ( + <span className="ml-2 text-[#75aafc] normal-case">shipped</span> + )} + {doc.status === "archived" && ( + <span className="ml-2 text-[#7788aa] normal-case">archived</span> + )} + {doc.status === "draft" && ( + <span className="ml-2 text-[#556677] normal-case">draft</span> + )} + {!!directive.orchestratorTaskId && ( + <span className="ml-auto inline-flex items-center gap-1 text-yellow-400"> + <span className="inline-block w-1.5 h-1.5 rounded-full bg-yellow-400 animate-pulse" /> + orchestrator running + </span> + )} + </div> + </div> - {revisionId ? ( - <RevisionViewer directiveId={directive.id} revisionId={revisionId} /> - ) : realTaskId ? ( - <EphemeralAwareTaskStream - taskId={realTaskId} - label={taskLabel ?? realTaskId.slice(0, 8)} - directive={directive} - /> - ) : ( <DocumentEditor - directive={directive} - onUpdateGoal={async (goal) => { - await updateGoal(goal); - }} + // Keying by document id ensures the Lexical editor remounts cleanly + // when the user switches documents, so the previous doc's body + // doesn't bleed into the new one. + key={doc.id} + directive={directiveAsDocument} + onUpdateGoal={onUpdateDocumentBody} onCleanup={async () => { await cleanup(); }} @@ -1387,7 +1003,33 @@ function EditorShell({ await pickUpOrders(); }} /> - )} + </div> + ); + } + + // --- Legacy fallback: directive selected but no document chosen -------- + // We only ever land here transiently while the page resolves the default + // document selection, so we render a thin "loading" placeholder rather + // than the full goal editor (which would be confusing alongside the new + // multi-document model). + return ( + <div className="flex-1 flex flex-col h-full overflow-hidden"> + <div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]"> + <div className="flex items-center gap-2 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]"> + <FileIcon /> + <span>directives /</span> + <span className="text-[#9bc3ff]"> + {directive.title.trim().length > 0 + ? directive.title + : directive.id.slice(0, 8)} + </span> + </div> + </div> + <div className="flex-1 flex items-center justify-center"> + <p className="text-[#556677] font-mono text-[12px]"> + Select a document, or click "+ New document" to create one. + </p> + </div> </div> ); } @@ -1396,25 +1038,31 @@ function EditorShell({ // Page // ============================================================================= -type ContextMenuState = - | { kind: "directive"; x: number; y: number; directive: DirectiveSummary } - | { - kind: "task"; - x: number; - y: number; - task: FolderTaskRow; - directiveId: string; - } - | null; - export default function DocumentDirectivesPage() { const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); const navigate = useNavigate(); - const { id: selectedId } = useParams<{ id: string }>(); + const { id: routeDirectiveId } = useParams<{ id: string }>(); const [searchParams, setSearchParams] = useSearchParams(); - const selectedTaskId = searchParams.get("task"); - const { directives, loading: listLoading, refresh: refreshList } = useDirectives(); - const [contextMenu, setContextMenu] = useState<ContextMenuState>(null); + const { directives, loading: listLoading } = useDirectives(); + + // refreshNonce — bumped to tell open directive folders to refetch their + // document lists (after a create or save). + const [refreshNonce, setRefreshNonce] = useState(0); + const bumpRefresh = useCallback(() => setRefreshNonce((n) => n + 1), []); + + // Derive the SidebarSelection from the URL. The route param is the + // directive id; ?document=:id and ?task=:id pick a specific child. Exactly + // one of taskId/documentId can be set; if both happen to be present in the + // URL (which shouldn't happen via our nav code) we prefer ?task= since + // task selection is the more disruptive action. + const selection: SidebarSelection | null = useMemo(() => { + if (!routeDirectiveId) return null; + const taskId = searchParams.get("task"); + const documentId = searchParams.get("document"); + if (taskId) return { directiveId: routeDirectiveId, taskId, documentId: null }; + if (documentId) return { directiveId: routeDirectiveId, taskId: null, documentId }; + return { directiveId: routeDirectiveId, taskId: null, documentId: null }; + }, [routeDirectiveId, searchParams]); useEffect(() => { if (!authLoading && isAuthConfigured && !isAuthenticated) { @@ -1422,96 +1070,86 @@ export default function DocumentDirectivesPage() { } }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); - const onSelect = useCallback( - (sel: SidebarSelection) => { - const next = `/directives/${sel.directiveId}${ - sel.taskId ? `?task=${sel.taskId}` : "" - }`; - navigate(next); - }, - [navigate], - ); + // ------------------------------------------------------------------ + // Default-selection: when the user clicks a directive's folder header (or + // lands on /directives/:id without ?document=) we pick the first active or + // draft document and update the URL to point at it. This avoids the + // "directive selected, but nothing in the editor" intermediate state. + // ------------------------------------------------------------------ + const lastResolvedRef = useRef<string | null>(null); + useEffect(() => { + if (!routeDirectiveId) { + lastResolvedRef.current = null; + return; + } + // Only auto-resolve when no document/task has been picked yet, AND we + // haven't already resolved this directive in a prior tick (otherwise + // navigating away from the doc would instantly re-pick the same one). + if (selection?.documentId || selection?.taskId) { + lastResolvedRef.current = routeDirectiveId; + return; + } + if (lastResolvedRef.current === routeDirectiveId) return; + lastResolvedRef.current = routeDirectiveId; - const onClearTask = useCallback(() => { - const next = new URLSearchParams(searchParams); - next.delete("task"); - setSearchParams(next, { replace: true }); - }, [searchParams, setSearchParams]); - - const onDirectiveContextMenu = useCallback( - (e: React.MouseEvent, d: DirectiveSummary) => { - e.preventDefault(); - e.stopPropagation(); - setContextMenu({ kind: "directive", x: e.clientX, y: e.clientY, directive: d }); - }, - [], - ); + let cancelled = false; + listDirectiveDocuments(routeDirectiveId) + .then((list) => { + if (cancelled) return; + // Prefer the first 'active' doc; fall back to the first 'draft'. + const firstActive = list.find((d) => d.status === "active"); + const firstDraft = list.find((d) => d.status === "draft"); + const pick = firstActive ?? firstDraft; + if (pick) { + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + next.set("document", pick.id); + next.delete("task"); + return next; + }, + { replace: true }, + ); + } + }) + .catch(() => { + // Swallow — the editor pane will show "Document not found" and the + // user can click "+ New document" to recover. + }); + return () => { + cancelled = true; + }; + }, [routeDirectiveId, selection?.documentId, selection?.taskId, setSearchParams]); - const onTaskContextMenu = useCallback( - (e: React.MouseEvent, task: FolderTaskRow, directiveId: string) => { - e.preventDefault(); - e.stopPropagation(); - setContextMenu({ kind: "task", x: e.clientX, y: e.clientY, task, directiveId }); + const handleSelectDocument = useCallback( + (directiveId: string, doc: DirectiveDocument) => { + navigate(`/directives/${directiveId}?document=${doc.id}`); }, - [], - ); - - const closeContextMenu = useCallback(() => setContextMenu(null), []); - - // Inline "+ New task" form state. When set, we render a small modal-ish - // overlay anchored to the directive folder; submitting calls the - // ephemeral-task endpoint. - const [newTaskFor, setNewTaskFor] = useState<DirectiveSummary | null>(null); - - const onCreateTask = useCallback((d: DirectiveSummary) => { - setNewTaskFor(d); - }, []); - - const handleSubmitNewTask = useCallback( - async (name: string, plan: string) => { - if (!newTaskFor) return; - try { - const task = await createDirectiveTask(newTaskFor.id, { name, plan }); - // Navigate the user into the freshly-spawned task's transcript. - navigate(`/directives/${newTaskFor.id}?task=${task.id}`); - setNewTaskFor(null); - } catch (err) { - // eslint-disable-next-line no-console - console.error("[makima] failed to create ephemeral task", err); - alert( - err instanceof Error - ? `Failed to create task: ${err.message}` - : "Failed to create task", - ); - } - }, - [newTaskFor, navigate], + [navigate], ); - const onQuickAction = useCallback( - async (d: DirectiveSummary, action: "start" | "pause" | "pr") => { - try { - if (action === "start") { - await startDirective(d.id); - } else if (action === "pause") { - await pauseDirective(d.id); - } else if (action === "pr") { - await createDirectivePR(d.id); - } - await refreshList(); - } catch (err) { - // eslint-disable-next-line no-console - console.error(`[makima] quick action ${action} failed`, err); - } + // When the user clicks a directive folder header (not a document row), we + // jump to /directives/:id without ?document= — the default-selection + // effect above will then pick the first active doc. + const handleSelectDirective = useCallback( + (directiveId: string) => { + if (routeDirectiveId === directiveId) return; + navigate(`/directives/${directiveId}`); }, - [refreshList], + [navigate, routeDirectiveId], ); - const onSelectOrphan = useCallback( - (taskId: string) => { - navigate(`/tmp/${taskId}`); + const handleCreateDocument = useCallback( + async (directive: DirectiveSummary) => { + const created = await createDirectiveDocument(directive.id, { + title: "", + body: "", + }); + bumpRefresh(); + // Navigate to the new doc so it's selected immediately. + navigate(`/directives/${directive.id}?document=${created.id}`); }, - [navigate], + [bumpRefresh, navigate], ); if (authLoading) { @@ -1525,10 +1163,6 @@ export default function DocumentDirectivesPage() { ); } - const selection: SidebarSelection | null = selectedId - ? { directiveId: selectedId, taskId: selectedTaskId } - : null; - return ( // h-screen + overflow-hidden so the page itself never scrolls; the // sidebar and editor pane each manage their own scroll via flex-1 @@ -1546,269 +1180,21 @@ export default function DocumentDirectivesPage() { directives={directives} loading={listLoading} selection={selection} - onSelect={onSelect} - onDirectiveContextMenu={onDirectiveContextMenu} - onTaskContextMenu={onTaskContextMenu} - onCreateTask={onCreateTask} - onQuickAction={onQuickAction} - onSelectOrphan={onSelectOrphan} + onSelectDocument={handleSelectDocument} + onSelectDirective={handleSelectDirective} + onCreateDocument={handleCreateDocument} + refreshNonce={refreshNonce} /> </div> - {/* Right: Lexical editor / task stream */} + {/* Right: Lexical editor */} <EditorShell - selectedId={selectedId} - selectedTaskId={selectedTaskId} + selection={selection} hasDirectives={directives.length > 0} listLoading={listLoading} - onClearTask={onClearTask} + onDocumentChanged={bumpRefresh} /> </main> - - {/* Context menus — rendered at page level so they overlay everything. */} - {contextMenu?.kind === "directive" && ( - <DirectiveContextMenu - x={contextMenu.x} - y={contextMenu.y} - directive={contextMenu.directive} - onClose={closeContextMenu} - onStart={async () => { - await startDirective(contextMenu.directive.id); - await refreshList(); - }} - onPause={async () => { - await pauseDirective(contextMenu.directive.id); - await refreshList(); - }} - onArchive={async () => { - await updateDirective(contextMenu.directive.id, { - status: "archived", - }); - await refreshList(); - }} - onDelete={async () => { - if ( - !window.confirm( - `Delete "${contextMenu.directive.title}"? This cannot be undone.`, - ) - ) { - return; - } - await deleteDirective(contextMenu.directive.id); - await refreshList(); - // If the deleted one was selected, clear selection. - if (selectedId === contextMenu.directive.id) { - navigate("/directives"); - } - }} - onGoToPR={() => { - if (contextMenu.directive.prUrl) { - window.open(contextMenu.directive.prUrl, "_blank", "noreferrer"); - } - }} - onNewDraft={async () => { - await newDirectiveDraft(contextMenu.directive.id); - await refreshList(); - // Send the user into the freshly-cleared contract so they can - // start typing the next iteration immediately. - navigate(`/directives/${contextMenu.directive.id}`); - }} - onCreatePR={async () => { - await createDirectivePR(contextMenu.directive.id); - await refreshList(); - }} - onAdvance={async () => { - await advanceDirective(contextMenu.directive.id); - await refreshList(); - }} - onCleanup={async () => { - await cleanupDirective(contextMenu.directive.id); - await refreshList(); - }} - onPickUpOrders={async () => { - await pickUpOrders(contextMenu.directive.id); - await refreshList(); - }} - /> - )} - {contextMenu?.kind === "task" && ( - <TaskContextMenu - x={contextMenu.x} - y={contextMenu.y} - task={contextMenu.task} - onClose={closeContextMenu} - onInterrupt={async () => { - try { - await stopTask(contextMenu.task.taskId); - } catch (err) { - // eslint-disable-next-line no-console - console.error("[makima] failed to interrupt task", err); - } - await refreshList(); - }} - onComplete={async () => { - if (!contextMenu.task.stepId) return; - await completeDirectiveStep( - contextMenu.directiveId, - contextMenu.task.stepId, - ); - await refreshList(); - }} - onFail={async () => { - if (!contextMenu.task.stepId) return; - await failDirectiveStep( - contextMenu.directiveId, - contextMenu.task.stepId, - ); - await refreshList(); - }} - onSkip={async () => { - if (!contextMenu.task.stepId) return; - await skipDirectiveStep( - contextMenu.directiveId, - contextMenu.task.stepId, - ); - await refreshList(); - }} - onSendMessage={async () => { - // Browser prompt is the lightest-weight surface that doesn't - // require redesigning a modal. The same comment box is also - // available below the live transcript when the task is selected. - const message = window.prompt("Send message to task:"); - if (!message || !message.trim()) return; - try { - await sendTaskMessage(contextMenu.task.taskId, message.trim()); - } catch (err) { - // eslint-disable-next-line no-console - console.error("[makima] failed to send task message", err); - } - }} - onOpenInTaskPage={() => { - // The standalone /exec/:taskId page has the full task UI with - // worktree diff viewer, checkpoint controls, etc. - navigate(`/exec/${contextMenu.task.taskId}`); - }} - /> - )} - - {newTaskFor && ( - <NewTaskModal - directive={newTaskFor} - onClose={() => setNewTaskFor(null)} - onSubmit={handleSubmitNewTask} - /> - )} - </div> - ); -} - -/** - * Inline "+ New task" form for spawning an ephemeral task under a - * directive. Surfaced as a centered modal, dismissible with Esc / click-out. - */ -function NewTaskModal({ - directive, - onClose, - onSubmit, -}: { - directive: DirectiveSummary; - onClose: () => void; - onSubmit: (name: string, plan: string) => Promise<void>; -}) { - const [name, setName] = useState(""); - const [plan, setPlan] = useState(""); - const [submitting, setSubmitting] = useState(false); - const nameRef = useRef<HTMLInputElement>(null); - - useEffect(() => { - nameRef.current?.focus(); - const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - document.addEventListener("keydown", onKey); - return () => document.removeEventListener("keydown", onKey); - }, [onClose]); - - const submit = async (e: React.FormEvent) => { - e.preventDefault(); - const trimmedName = name.trim(); - const trimmedPlan = plan.trim(); - if (!trimmedName || !trimmedPlan || submitting) return; - setSubmitting(true); - try { - await onSubmit(trimmedName, trimmedPlan); - } finally { - setSubmitting(false); - } - }; - - return ( - <div - className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" - onClick={onClose} - > - <form - onSubmit={submit} - onClick={(e) => e.stopPropagation()} - className="w-[480px] max-w-[90vw] bg-[#0a1628] border border-[rgba(117,170,252,0.4)] shadow-xl flex flex-col" - > - <div className="px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.25)]"> - <p className="text-[10px] font-mono text-[#556677] uppercase tracking-wide"> - New task in - </p> - <p className="text-[12px] font-mono text-white truncate"> - {directive.title} - </p> - </div> - <div className="px-4 py-4 space-y-3"> - <div className="space-y-1"> - <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide"> - Name - </label> - <input - ref={nameRef} - type="text" - value={name} - onChange={(e) => setName(e.target.value)} - placeholder="e.g. Investigate flaky test in auth.test.ts" - className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566]" - /> - </div> - <div className="space-y-1"> - <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide"> - Plan / instructions - </label> - <textarea - value={plan} - onChange={(e) => setPlan(e.target.value)} - onKeyDown={(e) => { - if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { - void submit(e as unknown as React.FormEvent); - } - }} - rows={5} - placeholder="What should the task do?" - className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566] resize-none" - /> - </div> - </div> - <div className="px-4 py-3 border-t border-dashed border-[rgba(117,170,252,0.25)] flex items-center justify-end gap-2"> - <button - type="button" - onClick={onClose} - className="px-3 py-1.5 text-[11px] font-mono uppercase tracking-wide text-[#7788aa] border border-[#2a3a5a] hover:text-white" - > - Cancel - </button> - <button - type="submit" - disabled={!name.trim() || !plan.trim() || submitting} - className="px-3 py-1.5 text-[11px] font-mono uppercase tracking-wide text-emerald-300 border border-emerald-700/60 hover:border-emerald-400 disabled:opacity-40 disabled:cursor-not-allowed" - > - {submitting ? "Creating…" : "Spawn task"} - </button> - </div> - </form> </div> ); } diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo index 56c723a..a520296 100644 --- a/makima/frontend/tsconfig.tsbuildinfo +++ b/makima/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/directives/doglist.tsx","./src/components/directives/directivecontextmenu.tsx","./src/components/directives/directivedag.tsx","./src/components/directives/directivedetail.tsx","./src/components/directives/directivelist.tsx","./src/components/directives/directivelogstream.tsx","./src/components/directives/documenteditor.tsx","./src/components/directives/orchestratorstepnode.tsx","./src/components/directives/stepnode.tsx","./src/components/directives/stepsblocknode.tsx","./src/components/directives/taskslideoutpanel.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/orders/ordercontextmenu.tsx","./src/components/orders/orderdetail.tsx","./src/components/orders/orderlist.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usedirectives.ts","./src/hooks/usedogs.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usemultitasksubscription.ts","./src/hooks/useorders.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useusersettings.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/daemons.tsx","./src/routes/directives.tsx","./src/routes/document-directives.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/orders.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/quickswitcher.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/directives/doglist.tsx","./src/components/directives/directivecontextmenu.tsx","./src/components/directives/directivedag.tsx","./src/components/directives/directivedetail.tsx","./src/components/directives/directivelist.tsx","./src/components/directives/directivelogstream.tsx","./src/components/directives/documenteditor.tsx","./src/components/directives/documenttaskstream.tsx","./src/components/directives/orchestratorstepnode.tsx","./src/components/directives/stepnode.tsx","./src/components/directives/stepsblocknode.tsx","./src/components/directives/taskslideoutpanel.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/orders/ordercontextmenu.tsx","./src/components/orders/orderdetail.tsx","./src/components/orders/orderlist.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usedirectives.ts","./src/hooks/usedogs.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usemultitasksubscription.ts","./src/hooks/useorders.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useusersettings.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/daemons.tsx","./src/routes/directives.tsx","./src/routes/document-directives.tsx","./src/routes/exec-redirect.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/orders.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/routes/tmp.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file |
