diff options
25 files changed, 514 insertions, 1784 deletions
diff --git a/frontend/index.html b/frontend/index.html index 0014d94..095310b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,7 +6,7 @@ <title>soryu.co</title> <link rel="icon" type="image/png" href="/logo/crane-logo-transparent.png" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> - <link href="https://fonts.googleapis.com/css2?family=DotGothic16&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet"> + <link href="https://fonts.googleapis.com/css2?family=DotGothic16&display=swap" rel="stylesheet"> </head> <body> <div id="root"></div> diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 771bdff..230ed07 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,13 +8,8 @@ "name": "pc98-vn", "version": "0.0.1", "dependencies": { - "@lexical/link": "^0.44.0", - "@lexical/list": "^0.44.0", - "@lexical/react": "^0.44.0", - "@lexical/rich-text": "^0.44.0", "@nanostores/react": "^1.0.0", "@types/three": "^0.180.0", - "lexical": "^0.21.0", "nanostores": "^1.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -71,6 +66,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -261,14 +257,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -722,241 +710,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@lexical/clipboard": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.21.0.tgz", - "integrity": "sha512-3lNMlMeUob9fcnRXGVieV/lmPbmet/SVWckNTOwzfKrZ/YW5HiiyJrWviLRVf50dGXTbmBGt7K/2pfPYvWCHFA==", - "dependencies": { - "@lexical/html": "0.21.0", - "@lexical/list": "0.21.0", - "@lexical/selection": "0.21.0", - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/code": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.21.0.tgz", - "integrity": "sha512-E0DNSFu4I+LMn3ft+UT0Dbntc8ZKjIA0BJj6BDewm0qh3bir40YUf5DkI2lpiFNRF2OpcmmcIxakREeU6avqTA==", - "dependencies": { - "@lexical/utils": "0.21.0", - "lexical": "0.21.0", - "prismjs": "^1.27.0" - } - }, - "node_modules/@lexical/devtools-core": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.21.0.tgz", - "integrity": "sha512-csK41CmRLZbKNV5pT4fUn5RzdPjU5PoWR8EqaS9kiyayhDg2zEnuPtvUYWanLfCLH9A2oOfbEsGxjMctAySlJw==", - "dependencies": { - "@lexical/html": "0.21.0", - "@lexical/link": "0.21.0", - "@lexical/mark": "0.21.0", - "@lexical/table": "0.21.0", - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - }, - "peerDependencies": { - "react": ">=17.x", - "react-dom": ">=17.x" - } - }, - "node_modules/@lexical/dragon": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.21.0.tgz", - "integrity": "sha512-ahTCaOtRFNauEzplN1qVuPjyGAlDd+XcVM5FQCdxVh/1DvqmBxEJRVuCBqatzUUVb89jRBekYUcEdnY9iNjvEQ==", - "dependencies": { - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/hashtag": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.21.0.tgz", - "integrity": "sha512-O4dxcZNq1Xm45HLoRifbGAYvQkg3qLoBc6ibmHnDqZL5mQDsufnH6QEKWfgDtrvp9++3iqsSC+TE7VzWIvA7ww==", - "dependencies": { - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/history": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.21.0.tgz", - "integrity": "sha512-Sv2sici2NnAfHYHYRSjjS139MDT8fHP6PlYM2hVr+17dOg7/fJl22VBLRgQ7/+jLtAPxQjID69jvaMlOvt4Oog==", - "dependencies": { - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/html": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.21.0.tgz", - "integrity": "sha512-UGahVsGz8OD7Ya39qwquE+JPStTxCw/uaQrnUNorCM7owtPidO2H+tsilAB3A1GK3ksFGdHeEjBjG0Gf7gOg+Q==", - "dependencies": { - "@lexical/selection": "0.21.0", - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/link": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.21.0.tgz", - "integrity": "sha512-/coktIyRXg8rXz/7uxXsSEfSQYxPIx8CmignAXWYhcyYtCWA0fD2mhEhWwVvHH9ofNzvidclRPYKUnrmUm3z3Q==", - "dependencies": { - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/list": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.21.0.tgz", - "integrity": "sha512-WItGlwwNJCS8b6SO1QPKzArShmD+OXQkLbhBcAh+EfpnkvmCW5T5LqY+OfIRmEN1dhDOnwqCY7mXkivWO8o5tw==", - "dependencies": { - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/mark": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.21.0.tgz", - "integrity": "sha512-2x/LoHDYPOkZbKHz4qLFWsPywjRv9KggTOtmRazmaNRUG0FpkImJwUbbaKjWQXeESVGpzfL3qNFSAmCWthsc4g==", - "dependencies": { - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/markdown": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.21.0.tgz", - "integrity": "sha512-XCQCyW5ujK0xR6evV8sF0hv/MRUA//kIrB2JiyF12tLQyjLRNEXO+0IKastWnMKSaDdJMKjzgd+4PiummYs7uA==", - "dependencies": { - "@lexical/code": "0.21.0", - "@lexical/link": "0.21.0", - "@lexical/list": "0.21.0", - "@lexical/rich-text": "0.21.0", - "@lexical/text": "0.21.0", - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/offset": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.21.0.tgz", - "integrity": "sha512-UR0wHg+XXbq++6aeUPdU0K41xhUDBYzX+AeiqU9bZ7yoOq4grvKD8KBr5tARCSYTy0yvQnL1ddSO12TrP/98Lg==", - "dependencies": { - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/overflow": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.21.0.tgz", - "integrity": "sha512-93P+d1mbvaJvZF8KK2pG22GuS2pHLtyC7N3GBfkbyAIb7TL/rYs47iR+eADJ4iNY680lylJ4Sl/AEnWvlY7hAg==", - "dependencies": { - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/plain-text": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.21.0.tgz", - "integrity": "sha512-r4CsAknBD7qGYSE5fPdjpJ6EjfvzHbDtuCeKciL9muiswQhw4HeJrT1qb/QUIY+072uvXTgCgmjUmkbYnxKyPA==", - "dependencies": { - "@lexical/clipboard": "0.21.0", - "@lexical/selection": "0.21.0", - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/react": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.21.0.tgz", - "integrity": "sha512-tKwx8EoNkBBKOZf8c10QfyDImH87+XUI1QDL8KXt+Lb8E4ho7g1jAjoEirNEn9gMBj33K4l2qVdbe3XmPAdpMQ==", - "dependencies": { - "@lexical/clipboard": "0.21.0", - "@lexical/code": "0.21.0", - "@lexical/devtools-core": "0.21.0", - "@lexical/dragon": "0.21.0", - "@lexical/hashtag": "0.21.0", - "@lexical/history": "0.21.0", - "@lexical/link": "0.21.0", - "@lexical/list": "0.21.0", - "@lexical/mark": "0.21.0", - "@lexical/markdown": "0.21.0", - "@lexical/overflow": "0.21.0", - "@lexical/plain-text": "0.21.0", - "@lexical/rich-text": "0.21.0", - "@lexical/selection": "0.21.0", - "@lexical/table": "0.21.0", - "@lexical/text": "0.21.0", - "@lexical/utils": "0.21.0", - "@lexical/yjs": "0.21.0", - "lexical": "0.21.0", - "react-error-boundary": "^3.1.4" - }, - "peerDependencies": { - "react": ">=17.x", - "react-dom": ">=17.x" - } - }, - "node_modules/@lexical/rich-text": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.21.0.tgz", - "integrity": "sha512-+pvEKUneEkGfWOSTl9jU58N9knePilMLxxOtppCAcgnaCdilOh3n5YyRppXhvmprUe0JaTseCMoik2LP51G/JA==", - "dependencies": { - "@lexical/clipboard": "0.21.0", - "@lexical/selection": "0.21.0", - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/selection": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.21.0.tgz", - "integrity": "sha512-4u53bc8zlPPF0rnHjsGQExQ1St8NafsDd70/t1FMw7yvoMtUsKdH7+ap00esLkJOMv45unJD7UOzKRqU1X0sEA==", - "dependencies": { - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/table": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.21.0.tgz", - "integrity": "sha512-JhylAWcf4qKD4FmxMUt3YzH5zg2+baBr4+/haLZL7178hMvUzJwGIiWk+3hD3phzmW3WrP49uFXzM7DMSCkE8w==", - "dependencies": { - "@lexical/clipboard": "0.21.0", - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/text": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.21.0.tgz", - "integrity": "sha512-ceB4fhYejCoR8ID4uIs0sO/VyQoayRjrRWTIEMvOcQtwUkcyciKRhY0A7f2wVeq/MFStd+ajLLjy4WKYK5zUnA==", - "dependencies": { - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/utils": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.21.0.tgz", - "integrity": "sha512-YzsNOAiLkCy6R3DuP18gtseDrzgx+30lFyqRvp5M7mckeYgQElwdfG5biNFDLv7BM9GjSzgU5Cunjycsx6Sjqg==", - "dependencies": { - "@lexical/list": "0.21.0", - "@lexical/selection": "0.21.0", - "@lexical/table": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/yjs": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.21.0.tgz", - "integrity": "sha512-AtPhC3pJ92CHz3dWoniSky7+MSK2WSd0xijc76I2qbTeXyeuFfYyhR6gWMg4knuY9Wz3vo9/+dXGdbQIPD8efw==", - "dependencies": { - "@lexical/offset": "0.21.0", - "@lexical/selection": "0.21.0", - "lexical": "0.21.0" - }, - "peerDependencies": { - "yjs": ">=13.5.22" - } - }, "node_modules/@nanostores/react": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@nanostores/react/-/react-1.0.0.tgz", @@ -1307,6 +1060,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~7.10.0" } @@ -1322,6 +1076,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "dev": true, + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1404,6 +1159,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -1576,32 +1332,6 @@ "node": ">=6" } }, - "node_modules/lexical": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.21.0.tgz", - "integrity": "sha512-Dxc5SCG4kB+wF+Rh55ism3SuecOKeOtCtGHFGKd6pj2QKVojtjkxGTQPMt7//2z5rMSue4R+hmRM0pCEZflupA==" - }, - "node_modules/lib0": { - "version": "0.2.117", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", - "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", - "peer": true, - "dependencies": { - "isomorphic.js": "^0.2.4" - }, - "bin": { - "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", - "0gentesthtml": "bin/gentesthtml.js", - "0serve": "bin/0serve.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1661,6 +1391,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -1705,18 +1436,11 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prismjs": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", - "engines": { - "node": ">=6" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -1728,6 +1452,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -1736,21 +1461,6 @@ "react": "^18.3.1" } }, - "node_modules/react-error-boundary": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", - "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - }, - "peerDependencies": { - "react": ">=16.13.1" - } - }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -1916,6 +1626,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/frontend/package.json b/frontend/package.json index 1ef66c3..197c3d8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,13 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/link": "^0.44.0", - "@lexical/list": "^0.44.0", - "@lexical/react": "^0.44.0", - "@lexical/rich-text": "^0.44.0", "@nanostores/react": "^1.0.0", "@types/three": "^0.180.0", - "lexical": "^0.21.0", "nanostores": "^1.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx index e7579b5..f126d6f 100644 --- a/frontend/src/components/LandingPage.tsx +++ b/frontend/src/components/LandingPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' import { LoadingScreen } from './LoadingScreen' import { HeartLogo } from './HeartLogo' @@ -9,7 +9,12 @@ interface LandingPageProps { export function LandingPage({ onLogin }: LandingPageProps) { const [loading, setLoading] = useState(false) const [showLanding, setShowLanding] = useState(false) + const [isStandby, setIsStandby] = useState(true) // false = LIVE, true = STDBY + const [velocity, setVelocity] = useState(0) + const [energy, setEnergy] = useState(0) + const [ramped, setRamped] = useState(false) const [pendingAction, setPendingAction] = useState<null | 'makimaRedirect'>(null) + const [activePanel, setActivePanel] = useState<null | 'mission' | 'makima'>(null) // Fade-in landing page content after mount useEffect(() => { @@ -17,6 +22,62 @@ export function LandingPage({ onLogin }: LandingPageProps) { return () => clearTimeout(timer) }, []) + // Ramp up stats, then keep them fluctuating near max + useEffect(() => { + const VELOCITY_MAX = 603 + const ENERGY_MAX = 32 + + let rampInterval: number | undefined + let fluctuateInterval: number | undefined + + // Ramp-up for ~2 seconds + const rampDurationMs = 2000 + const tickMs = 30 + const vStep = VELOCITY_MAX / (rampDurationMs / tickMs) + const eStep = ENERGY_MAX / (rampDurationMs / tickMs) + + rampInterval = window.setInterval(() => { + setVelocity((v) => { + const next = v + vStep + return next >= VELOCITY_MAX ? VELOCITY_MAX : next + }) + setEnergy((e) => { + const next = e + eStep + return next >= ENERGY_MAX ? ENERGY_MAX : next + }) + }, tickMs) + + const stopRamp = window.setTimeout(() => { + if (rampInterval) window.clearInterval(rampInterval) + setVelocity(VELOCITY_MAX) + setEnergy(ENERGY_MAX) + setRamped(true) + + // Fluctuate near the top + fluctuateInterval = window.setInterval(() => { + setVelocity((v) => { + const min = VELOCITY_MAX - 18 + const max = VELOCITY_MAX + const delta = (Math.random() - 0.5) * 6 // ±3 + const next = Math.max(min, Math.min(max, v + delta)) + return next + }) + setEnergy((e) => { + const min = ENERGY_MAX - 2 + const max = ENERGY_MAX + const delta = (Math.random() - 0.5) * 0.25 // ±0.125 + const next = Math.max(min, Math.min(max, e + delta)) + return next + }) + }, 220) + }, rampDurationMs + 60) + + return () => { + if (rampInterval) window.clearInterval(rampInterval) + if (fluctuateInterval) window.clearInterval(fluctuateInterval) + } + }, []) + const handleLoadingComplete = () => { if (pendingAction === 'makimaRedirect') { window.location.assign('https://makima.jp') @@ -30,129 +91,144 @@ export function LandingPage({ onLogin }: LandingPageProps) { setLoading(true) } - const scrollTo = (id: string) => { - document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' }) + const handleMission = () => { + setActivePanel((mode) => (mode === 'mission' ? null : 'mission')) + } + + const handleMakimaPanel = () => { + setActivePanel((mode) => (mode === 'makima' ? null : 'makima')) } return ( <div> {loading && <LoadingScreen onComplete={handleLoadingComplete} />} - {/* Professional floating header */} {!loading && ( - <div className={`pro-header ${showLanding ? 'fade-in' : 'hidden'}`}> - <div className="pro-header-content"> - <div className="pro-header-left"> + <div className={`floating-header ${showLanding ? 'fade-in' : 'hidden'}`}> + <div className="header-content"> + <div className="brand"> <img src="/logo/crane-logo-transparent.png" alt="Soryu" - height={36} - className="pro-crane-logo" + height={40} + className="brand-mark" onError={(e) => { const img = e.currentTarget as HTMLImageElement img.onerror = null img.src = '/logo/crane-logo.png' }} /> - <span className="pro-company-name">SORYU</span> </div> - - <div className="pro-header-center"> + <div className="header-center"> <HeartLogo size="header-no-rays" className="header-heart" /> </div> - <nav className="pro-header-nav"> - <button className="pro-nav-link" onClick={() => scrollTo('pro-mission')}> - Mission - </button> - <button className="pro-nav-link" onClick={() => scrollTo('pro-makima')}> - Makima - </button> - <button className="pro-nav-link pro-nav-login" onClick={handleLogin}> - Login - </button> - </nav> + <div className="system-info"> + <div className="info-item"> + <span className="info-label">System:</span> + <span + className="info-value live-status clickable" + onClick={() => setIsStandby(!isStandby)} + title="Click to toggle between LIVE and STANDBY" + > + <span className={`status-dot ${isStandby ? 'standby' : 'live'}`}></span> + {isStandby ? 'STDBY' : 'LIVE'} + </span> + </div> + <div className="info-item"> + <span className="info-label">Version:</span> + <span className="info-value">v1.0.0</span> + </div> + </div> </div> </div> )} - {/* Main professional landing layout */} - <div className={`pro-landing ${showLanding && !loading ? 'fade-in' : 'hidden'}`}> - - {/* Hero section */} - <section className="pro-hero"> - <div className="pro-hero-inner"> - <div className="pro-hero-tagline-jp">そりゅう</div> - <h1 className="pro-hero-headline"> - Real‑Time Systems for<br />Mission‑Critical Observability - </h1> - <p className="pro-hero-sub"> - Low‑latency streaming infrastructure that turns live data into reliable, secure insight. - </p> - <div className="pro-hero-cta"> - <button className="pro-btn-primary" onClick={() => scrollTo('pro-mission')}> - Learn More - </button> - <button className="pro-btn-secondary" onClick={handleLogin}> - <span className="pro-btn-icon">▶</span> Launch Makima - </button> - </div> - </div> - </section> - - {/* Content grid: Mission + Makima cards */} - <section className="pro-content-grid"> - <div className="pro-card" id="pro-mission"> - <div className="pro-card-header"> - <h2 className="pro-card-title">Mission</h2> - <div className="pro-card-accent" /> + <div className={`modern-landing-page manga-style ${showLanding && !loading ? 'fade-in' : 'hidden'}`}> + {/* Background GIF fills the main body */} + <div className="background-only" aria-hidden="true"> + <img src="/background-animation.gif" alt="" className="background-gif" /> + </div> + + {/* Minimal overlay: masthead, issue badge, and CTA */} + <div className={`taisho-cover ${activePanel ? 'mission-mode' : ''}`}> + <div className="cover-backdrop" aria-hidden="true" /> + <div className="cover-content"> + {/* Masthead + Issue badge (kept) */} + <div className="masthead"> + <div className="masthead-vertical"> + <span className="jp">そりゅう</span> + <span className="en">SORYU</span> + </div> + <div className="issue-badge"><span className="led-heart" aria-hidden="true"></span>かはいい Vol.01</div> </div> - <div className="pro-card-body"> - <h3 className="pro-card-subtitle"> - Building real‑time systems for mission-critical observability and surveillance - </h3> - <p className="pro-card-text"> - We deliver low‑latency streaming & infrastructure that turns live data into - reliable, secure insight. Target selection, monitoring and full end to end observability - to make vital decisions where it matters most. - </p> + + {/* Hero area becomes Mission content when in mission mode */} + {activePanel === 'mission' ? ( + <div className="mission-screen" role="region" aria-label="Mission"> + <h1 className="mission-headline">Building real‑time systems for mission-critical observability and surveillance </h1> + <img src="/PC98Doukuusei.webp" alt="Mission montage" className="mission-image" /> + <p className="mission-paragraph"> + We deliver low‑latency streaming & infrastructure that turns live data into + reliable, secure insight. Target selection, monitoring and full end to end observability + to make vital decisions where it matters most. + </p> + </div> + ) : activePanel === 'makima' ? ( + <div className="mission-screen makima-screen" role="region" aria-label="Makima"> + <h1 className="mission-headline makima-headline">Mesh Orchestration Platform</h1> + <span className="makima-badge">Control System</span> + <img src="/logo/makima-logo.svg" alt="Makima mesh logo" className="mission-image makima-logo" /> + <p className="mission-paragraph"> + Makima is a control system for orchestrating distributed daemon meshes, + coordinating concurrent execution across distinct domains. + </p> + <p className="mission-paragraph"> + Unified command interface for spawning, monitoring, and directing + worker daemons. Real-time task coordination with overlay management. + </p> + </div> + ) : ( + <div className="hero" /> + )} + + {/* CTA row spanning full width: left Mission/MAKIMA, right Login */} + <div className="cta-area"> + <div className="cta-left"> + <button className="taisho-cta" onClick={handleMission}> + <span className="cta-text">{activePanel === 'mission' ? 'Close' : 'Mission'}</span> + </button> + <button className="taisho-cta" onClick={handleMakimaPanel}> + <span className="cta-text">{activePanel === 'makima' ? 'Close' : 'MAKIMA'}</span> + </button> + </div> + <div className="cta-right"> + <button className="taisho-cta" onClick={handleLogin}> + <span className="cta-icon">▶</span> + <span className="cta-text">Login</span> + </button> + </div> </div> </div> + </div> - <div className="pro-card" id="pro-makima"> - <div className="pro-card-header"> - <h2 className="pro-card-title">Makima</h2> - <span className="pro-card-badge">Control System</span> - <div className="pro-card-accent" /> - </div> - <div className="pro-card-body"> - <img - src="/logo/makima-logo.svg" - alt="Makima mesh logo" - className="pro-makima-logo" - /> - <h3 className="pro-card-subtitle">Mesh Orchestration Platform</h3> - <p className="pro-card-text"> - Makima is a control system for orchestrating distributed daemon meshes, - coordinating concurrent execution across distinct domains. - </p> - <p className="pro-card-text"> - Unified command interface for spawning, monitoring, and directing - worker daemons. Real-time task coordination with overlay management. - </p> + {/* Bottom stats: Velocity + Energy only (hidden in mission mode) */} + {!activePanel && ( + <div className="bottom-stats"> + <div className="rf-stats"> + <div className="rf-stat"> + <span className="label">Velocity</span> + <span className="value">{Math.round(velocity)} km/h</span> + </div> + <div className="rf-stat"> + <span className="label">Energy</span> + <span className="value">{energy.toFixed(1)} MJ</span> + </div> </div> </div> - </section> - - {/* Footer */} - <footer className="pro-footer"> - <div className="pro-footer-inner"> - <span className="pro-footer-brand">SORYU</span> - <span className="pro-footer-sep">—</span> - <span className="pro-footer-text">Real‑time systems & infrastructure</span> - </div> - </footer> + )} </div> + {/* MissionDrawer removed in favor of mission screen transformation */} </div> ) } diff --git a/frontend/src/components/VNInterface.tsx b/frontend/src/components/VNInterface.tsx index 0a77f39..318a9b9 100644 --- a/frontend/src/components/VNInterface.tsx +++ b/frontend/src/components/VNInterface.tsx @@ -9,11 +9,9 @@ import { showSettingsModalStore, isVisibleStore, yenBalanceStore, - documentEditorEnabledStore, toggleStandby, toggleShowChoices, - updateTime, - setDocumentEditorEnabled + updateTime } from '../stores' interface VNInterfaceProps { @@ -28,7 +26,6 @@ export function VNInterface({ onLogout }: VNInterfaceProps) { const showSettingsModal = useStore(showSettingsModalStore) const isVisible = useStore(isVisibleStore) const yenBalance = useStore(yenBalanceStore) - const documentEditorEnabled = useStore(documentEditorEnabledStore) // Fade in effect on mount useEffect(() => { @@ -116,14 +113,6 @@ export function VNInterface({ onLogout }: VNInterfaceProps) { <span className="info-value">Contracts</span> </Link> </div> - {documentEditorEnabled && ( - <div className="status-item"> - <Link to="/directives" style={{ color: '#66ccff', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: '4px' }}> - <span className="info-label">Edit:</span> - <span className="info-value">Directives</span> - </Link> - </div> - )} </div> </div> </div> @@ -209,21 +198,6 @@ export function VNInterface({ onLogout }: VNInterfaceProps) { </div> </div> <div className="settings-section"> - <h3>Feature Flags</h3> - <div className="setting-item"> - <label> - <input - type="checkbox" - checked={documentEditorEnabled} - onChange={(e) => setDocumentEditorEnabled(e.target.checked)} - /> Document Editor (Directives) - </label> - <div style={{ fontSize: '0.8em', opacity: 0.7, marginTop: '4px' }}> - Enable the directive document editor interface - </div> - </div> - </div> - <div className="settings-section"> <h3>Audio</h3> <div className="setting-item"> <label>Master Volume</label> diff --git a/frontend/src/components/document/index.ts b/frontend/src/components/document/index.ts deleted file mode 100644 index 906c1dc..0000000 --- a/frontend/src/components/document/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export { default as DocumentLayout } from './DocumentLayout' -export { default as DocumentEditor } from './DocumentEditor' -export { DirectiveFileTree } from './DirectiveFileTree' -export { default as DocumentSettings } from './DocumentSettings' - -// Lexical Nodes -export { StepsDiagramNode, $createStepsDiagramNode, $isStepsDiagramNode } from './nodes/StepsDiagramNode' -export { ContractBlockNode, $createContractBlockNode, $isContractBlockNode } from './nodes/ContractBlockNode' - -// Sub-components -export { StepsDiagramComponent } from './nodes/StepsDiagramComponent' -export { ContractBlockComponent } from './nodes/ContractBlockComponent' -export { StepLogFeed } from './nodes/StepLogFeed' -export { ContractLogFeed } from './nodes/ContractLogFeed' diff --git a/frontend/src/components/document/nodes/StepLogFeed.tsx b/frontend/src/components/document/nodes/StepLogFeed.tsx deleted file mode 100644 index 2f2f553..0000000 --- a/frontend/src/components/document/nodes/StepLogFeed.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import React, { useEffect, useRef, useState, useCallback } from 'react'; - -interface StepLogFeedProps { - taskId: string; - stepName: string; - stepStatus: string; - onCollapse: () => void; -} - -interface LogEntry { - timestamp: string; - content: string; - type: 'stdout' | 'stderr' | 'system' | 'user'; -} - -/** - * Live log feed for an expanded step row. - * Connects via WebSocket to stream task output and allows - * sending messages (comments) and interrupting the task. - */ -export function StepLogFeed({ taskId, stepName, stepStatus, onCollapse }: StepLogFeedProps) { - const [logs, setLogs] = useState<LogEntry[]>([]); - const [message, setMessage] = useState(''); - const [sending, setSending] = useState(false); - const [connected, setConnected] = useState(false); - const [error, setError] = useState<string | null>(null); - const logsEndRef = useRef<HTMLDivElement>(null); - const wsRef = useRef<WebSocket | null>(null); - const logContainerRef = useRef<HTMLDivElement>(null); - const inputRef = useRef<HTMLInputElement>(null); - - const isActive = ['running', 'starting'].includes(stepStatus.toLowerCase()); - - // Auto-scroll to bottom when new logs arrive - useEffect(() => { - logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [logs]); - - // Connect to WebSocket for live streaming - useEffect(() => { - if (!taskId) return; - - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/api/v1/mesh/tasks/subscribe`; - - let ws: WebSocket; - let reconnectTimer: ReturnType<typeof setTimeout>; - let shouldReconnect = true; - - function connect() { - try { - ws = new WebSocket(wsUrl); - wsRef.current = ws; - - ws.addEventListener('open', () => { - setConnected(true); - setError(null); - // Subscribe to this specific task - ws.send(JSON.stringify({ type: 'subscribe', taskId })); - }); - - ws.addEventListener('message', (evt) => { - try { - const data = JSON.parse(evt.data); - // Handle different message formats from the backend - if (data.taskId === taskId || data.task_id === taskId) { - const entry: LogEntry = { - timestamp: data.timestamp || new Date().toISOString(), - content: data.content || data.output || data.message || JSON.stringify(data), - type: data.type || data.stream || 'stdout', - }; - setLogs(prev => [...prev, entry]); - } - } catch { - // Non-JSON message, treat as raw log - setLogs(prev => [...prev, { - timestamp: new Date().toISOString(), - content: evt.data, - type: 'stdout', - }]); - } - }); - - ws.addEventListener('close', () => { - setConnected(false); - wsRef.current = null; - if (shouldReconnect && isActive) { - reconnectTimer = setTimeout(connect, 3000); - } - }); - - ws.addEventListener('error', () => { - setConnected(false); - setError('WebSocket connection failed'); - }); - } catch (err) { - setError('Failed to connect to log stream'); - } - } - - connect(); - - return () => { - shouldReconnect = false; - clearTimeout(reconnectTimer); - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; - } - }; - }, [taskId, isActive]); - - // Keyboard shortcut: Escape to collapse - useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onCollapse(); - } - }; - document.addEventListener('keydown', handler); - return () => document.removeEventListener('keydown', handler); - }, [onCollapse]); - - // Send a message/comment to the task - const handleSendMessage = useCallback(async () => { - if (!message.trim() || !taskId || sending) return; - - setSending(true); - try { - const response = await fetch(`/api/v1/mesh/tasks/${taskId}/message`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message: message.trim() }), - }); - - if (!response.ok) { - const body = await response.json().catch(() => ({ message: response.statusText })); - throw new Error(body.message || body.error || `HTTP ${response.status}`); - } - - // Add as a user message in the log - setLogs(prev => [...prev, { - timestamp: new Date().toISOString(), - content: message.trim(), - type: 'user', - }]); - setMessage(''); - inputRef.current?.focus(); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to send message'); - } finally { - setSending(false); - } - }, [message, taskId, sending]); - - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSendMessage(); - } - // Prevent Escape from bubbling when input is focused - if (e.key === 'Escape') { - e.stopPropagation(); - inputRef.current?.blur(); - } - }, [handleSendMessage]); - - // Interrupt the running task - const handleInterrupt = useCallback(async () => { - if (!taskId) return; - try { - // Send a special interrupt message - const response = await fetch(`/api/v1/mesh/tasks/${taskId}/message`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message: '/interrupt' }), - }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - setLogs(prev => [...prev, { - timestamp: new Date().toISOString(), - content: 'Interrupt signal sent', - type: 'system', - }]); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to interrupt'); - } - }, [taskId]); - - const formatTimestamp = (ts: string) => { - try { - return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); - } catch { - return ''; - } - }; - - return ( - <div className="step-log-feed"> - {/* Header */} - <div className="step-log-feed-header"> - <div className="step-log-feed-header-left"> - <span className="step-log-feed-title">{stepName} - Logs</span> - <span className={`step-log-feed-status ${connected ? 'connected' : 'disconnected'}`}> - {connected ? 'Live' : 'Disconnected'} - </span> - </div> - <div className="step-log-feed-header-right"> - {isActive && ( - <button - className="step-log-feed-interrupt-btn" - onClick={handleInterrupt} - title="Interrupt this contract" - > - ⏹ Interrupt - </button> - )} - <button - className="step-log-feed-collapse-btn" - onClick={onCollapse} - title="Collapse (Esc)" - > - ✕ - </button> - </div> - </div> - - {/* Log content */} - <div className="step-log-feed-content" ref={logContainerRef}> - {logs.length === 0 && !error && ( - <div className="step-log-feed-empty"> - {isActive - ? 'Waiting for log output...' - : 'No logs available for this step.'} - </div> - )} - - {error && ( - <div className="step-log-feed-error">{error}</div> - )} - - {logs.map((entry, idx) => ( - <div key={idx} className={`step-log-entry step-log-entry--${entry.type}`}> - <span className="step-log-entry-time">{formatTimestamp(entry.timestamp)}</span> - <span className="step-log-entry-content">{entry.content}</span> - </div> - ))} - <div ref={logsEndRef} /> - </div> - - {/* Message input (comment/interrupt controls) */} - {isActive && ( - <div className="step-log-feed-input"> - <input - ref={inputRef} - type="text" - className="step-log-feed-input-field" - placeholder="Send a message to this contract..." - value={message} - onChange={(e) => setMessage(e.target.value)} - onKeyDown={handleKeyDown} - disabled={sending} - /> - <button - className="step-log-feed-send-btn" - onClick={handleSendMessage} - disabled={!message.trim() || sending} - title="Send message (Enter)" - > - {sending ? '...' : '➤'} - </button> - </div> - )} - </div> - ); -} diff --git a/frontend/src/components/document/nodes/StepsDiagramComponent.tsx b/frontend/src/components/document/nodes/StepsDiagramComponent.tsx deleted file mode 100644 index ac1cb83..0000000 --- a/frontend/src/components/document/nodes/StepsDiagramComponent.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import React, { useEffect, useState, useRef, useCallback } from 'react'; -import { getDirective, DirectiveStep, DirectiveWithSteps } from '../../../services/directiveApi'; -import { StepLogFeed } from './StepLogFeed'; -import './StepsDiagram.css'; - -interface StepsDiagramComponentProps { - directiveId: string; - onExpandContract?: (step: DirectiveStep) => void; -} - -type StepStatus = 'pending' | 'ready' | 'running' | 'completed' | 'failed' | 'skipped'; - -const STATUS_LABELS: Record<string, string> = { - pending: 'Queued', - ready: 'Ready', - running: 'Executing', - completed: 'Fulfilled', - failed: 'Failed', - skipped: 'Skipped', -}; - -function formatTime(dateStr: string): string { - if (!dateStr) return ''; - const d = new Date(dateStr); - return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); -} - -interface StepCardProps { - step: DirectiveStep; - isExpanded: boolean; - onToggleExpand: () => void; - onCollapse: () => void; -} - -function StepCard({ step, isExpanded, onToggleExpand, onCollapse }: StepCardProps) { - const status = (step.status || 'pending').toLowerCase() as StepStatus; - const hasTask = !!step.taskId || !!step.contractId; - const canExpand = hasTask && ['running', 'completed', 'failed'].includes(status); - - return ( - <div className={`steps-diagram-card steps-diagram-card--${status} ${isExpanded ? 'steps-diagram-card--expanded' : ''}`}> - <div - className={`steps-diagram-card-header ${canExpand ? 'steps-diagram-card-header--clickable' : ''}`} - onClick={canExpand ? onToggleExpand : undefined} - role={canExpand ? 'button' : undefined} - tabIndex={canExpand ? 0 : undefined} - onKeyDown={canExpand ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggleExpand(); } } : undefined} - > - <span className="steps-diagram-card-name">{step.name}</span> - <div className="steps-diagram-card-header-right"> - <span className={`steps-diagram-status-badge steps-diagram-status-badge--${status}`}> - {STATUS_LABELS[status] || status} - </span> - {canExpand && ( - <span className={`steps-diagram-expand-icon ${isExpanded ? 'expanded' : ''}`}> - ▶ - </span> - )} - </div> - </div> - {step.description && !isExpanded && ( - <p className="steps-diagram-card-desc">{step.description}</p> - )} - <div className="steps-diagram-card-footer"> - <span className="steps-diagram-card-index">#{step.orderIndex}</span> - {status === 'running' && ( - <span className="steps-diagram-card-progress">In progress...</span> - )} - {status === 'completed' && step.completedAt && ( - <span className="steps-diagram-card-time"> - Completed {formatTime(step.completedAt)} - </span> - )} - </div> - - {/* Expandable log feed */} - {isExpanded && hasTask && ( - <StepLogFeed - taskId={step.taskId || step.contractId} - stepName={step.name} - stepStatus={status} - onCollapse={onCollapse} - /> - )} - </div> - ); -} - -export function StepsDiagramComponent({ directiveId, onExpandContract }: StepsDiagramComponentProps) { - const [steps, setSteps] = useState<DirectiveStep[]>([]); - const [directiveStatus, setDirectiveStatus] = useState<string>(''); - const [loading, setLoading] = useState(true); - const [error, setError] = useState<string | null>(null); - const [expandedStepId, setExpandedStepId] = useState<string | null>(null); - const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null); - const prevStepCountRef = useRef(0); - - const fetchSteps = useCallback(async () => { - try { - const data: DirectiveWithSteps = await getDirective(directiveId); - setSteps(data.steps || []); - setDirectiveStatus(data.status || ''); - setError(null); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load contracts'); - } finally { - setLoading(false); - } - }, [directiveId]); - - useEffect(() => { - fetchSteps(); - intervalRef.current = setInterval(fetchSteps, 5000); - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [fetchSteps]); - - // Track when new steps appear for animation - useEffect(() => { - prevStepCountRef.current = steps.length; - }, [steps.length]); - - // Keyboard shortcut: Escape to collapse expanded step - useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (e.key === 'Escape' && expandedStepId) { - setExpandedStepId(null); - } - }; - document.addEventListener('keydown', handler); - return () => document.removeEventListener('keydown', handler); - }, [expandedStepId]); - - const toggleExpand = useCallback((stepId: string) => { - setExpandedStepId(prev => prev === stepId ? null : stepId); - }, []); - - const collapseExpanded = useCallback(() => { - setExpandedStepId(null); - }, []); - - const completedCount = steps.filter(s => s.status?.toLowerCase() === 'completed').length; - const totalCount = steps.length; - const isActive = ['active', 'running', 'planning'].includes(directiveStatus.toLowerCase()); - const isBuilding = isActive && steps.length === 0; - - // Group steps by orderIndex - const groupedSteps: Map<number, DirectiveStep[]> = new Map(); - const sortedSteps = [...steps].sort((a, b) => a.orderIndex - b.orderIndex); - for (const step of sortedSteps) { - const idx = step.orderIndex; - if (!groupedSteps.has(idx)) groupedSteps.set(idx, []); - groupedSteps.get(idx)!.push(step); - } - const orderGroups = Array.from(groupedSteps.entries()).sort((a, b) => a[0] - b[0]); - - if (loading) { - return ( - <div className="steps-diagram" contentEditable={false}> - <div className="steps-diagram-header"> - <span className="steps-diagram-header-title">Contract Steps</span> - <span className="steps-diagram-header-author">Authored by Makima</span> - </div> - <div className="steps-diagram-loading"> - <div className="steps-diagram-spinner" /> - <span>Loading contracts...</span> - </div> - </div> - ); - } - - if (error) { - return ( - <div className="steps-diagram" contentEditable={false}> - <div className="steps-diagram-header"> - <span className="steps-diagram-header-title">Contract Steps</span> - <span className="steps-diagram-header-author">Authored by Makima</span> - </div> - <div className="steps-diagram-error">Failed to load contracts: {error}</div> - </div> - ); - } - - return ( - <div className={`steps-diagram ${expandedStepId ? 'steps-diagram--has-expanded' : ''}`} contentEditable={false}> - <div className="steps-diagram-header"> - <div className="steps-diagram-header-left"> - <span className="steps-diagram-header-title">Contract Steps</span> - {totalCount > 0 && ( - <span className="steps-diagram-header-count"> - {completedCount}/{totalCount} fulfilled - </span> - )} - </div> - <span className="steps-diagram-header-author">Authored by Makima</span> - </div> - - {isBuilding && ( - <div className="steps-diagram-planning"> - <div className="steps-diagram-planning-dots"> - <span /><span /><span /> - </div> - <span>Makima is drafting contracts...</span> - </div> - )} - - {totalCount === 0 && !isBuilding && ( - <div className="steps-diagram-empty">No contract steps defined yet.</div> - )} - - {totalCount > 0 && ( - <div className="steps-diagram-dag"> - {orderGroups.map(([orderIndex, groupSteps], groupIdx) => ( - <React.Fragment key={orderIndex}> - {groupIdx > 0 && ( - <div className="steps-diagram-arrow"> - <div className="steps-diagram-arrow-line" /> - <div className="steps-diagram-arrow-head" /> - </div> - )} - <div className="steps-diagram-group"> - {groupSteps.map((step) => ( - <StepCard - key={step.id} - step={step} - isExpanded={expandedStepId === step.id} - onToggleExpand={() => toggleExpand(step.id)} - onCollapse={collapseExpanded} - /> - ))} - </div> - </React.Fragment> - ))} - </div> - )} - </div> - ); -} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 7688159..04b8cde 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -14,8 +14,6 @@ import './styles/mobile.css' // - /contracts - List all contracts // - /contracts/:id - View contract details with tabs (including Files tab) // - /contracts/:contractId/files/:fileId - View a specific file within contract context -// - /directives - Document editor layout (file tree sidebar) -// - /directives/:id - Document editor for a specific directive // // Note: Standalone file routes (/files, /files/:id) have been removed. // Files are now only accessible through their parent contract. @@ -38,14 +36,6 @@ const router = createBrowserRouter([ element: <FileDetail />, }, { - path: '/directives', - element: <DocumentLayout />, - }, - { - path: '/directives/:id', - element: <DocumentLayout />, - }, - { path: '/daemons', element: <DaemonList />, }, diff --git a/frontend/src/services/directiveApi.ts b/frontend/src/services/directiveApi.ts deleted file mode 100644 index fcb6636..0000000 --- a/frontend/src/services/directiveApi.ts +++ /dev/null @@ -1,185 +0,0 @@ -// API service for directive operations - -export interface DirectiveStepCounts { - pending: number - ready: number - running: number - completed: number - failed: number - skipped: number -} - -export interface DirectiveSummary { - id: string - title: string - goal: string - status: string - repositoryUrl: string - prUrl: string - prBranch: string - createdAt: string - updatedAt: string - goalUpdatedAt: string - stepCounts: DirectiveStepCounts - version?: number - pr_branch?: string | null -} - -export interface DirectiveStep { - id: string - directiveId: string - directive_id?: string - name: string - title?: string - description: string | null - taskPlan: string - dependsOn: string[] - status: string - contractId: string - /** @deprecated Use contractId instead */ - taskId: string - orderIndex: number - sort_order?: number - completedAt: string -} - -export interface DirectiveWithSteps extends DirectiveSummary { - steps: DirectiveStep[] - reconcileMode: string -} - -// Alias for compatibility with context-menu branch types -export type Directive = DirectiveSummary - -async function apiFetch(path: string, options?: RequestInit): Promise<Response> { - const response = await fetch(path, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers, - }, - }) - if (!response.ok) { - const body = await response.json().catch(() => ({ message: response.statusText })) - throw new Error(body.message ?? body.error ?? `API error ${response.status}: ${response.statusText}`) - } - return response -} - -export async function listDirectives(): Promise<DirectiveSummary[]> { - const response = await apiFetch('/api/v1/directives') - const data = await response.json() - return data.directives || [] -} - -export async function getDirective(id: string): Promise<DirectiveWithSteps> { - const response = await apiFetch(`/api/v1/directives/${id}`) - return response.json() -} - -export async function getDirectiveSteps(id: string): Promise<DirectiveStep[]> { - const response = await apiFetch(`/api/v1/directives/${id}/steps`) - return response.json() -} - -export async function updateGoal(id: string, goal: string): Promise<DirectiveWithSteps> { - const response = await apiFetch(`/api/v1/directives/${id}/goal`, { - method: 'PUT', - body: JSON.stringify({ goal }), - }) - return response.json() -} - -export async function updateDirective( - id: string, - updates: { title?: string; goal?: string; version?: number }, -): Promise<DirectiveWithSteps> { - const response = await apiFetch(`/api/v1/directives/${id}`, { - method: 'PUT', - body: JSON.stringify(updates), - }) - return response.json() -} - -export async function cleanupDirective(id: string): Promise<void> { - await apiFetch(`/api/v1/directives/${id}/cleanup`, { method: 'POST' }) -} - -export async function createPr(id: string): Promise<{ prUrl: string }> { - const response = await apiFetch(`/api/v1/directives/${id}/create-pr`, { method: 'POST' }) - return response.json() -} - -export async function pickUpOrders(id: string): Promise<void> { - await apiFetch(`/api/v1/directives/${id}/pick-up-orders`, { method: 'POST' }) -} - -export async function startDirective(id: string): Promise<DirectiveWithSteps> { - const response = await apiFetch(`/api/v1/directives/${id}/start`, { method: 'POST' }) - return response.json() -} - -export async function pauseDirective(id: string): Promise<DirectiveWithSteps> { - const response = await apiFetch(`/api/v1/directives/${id}/pause`, { method: 'POST' }) - return response.json() -} - -export async function getUserSetting(key: string): Promise<any> { - const response = await apiFetch(`/api/v1/user-settings/${key}`) - return response.json() -} - -export async function upsertUserSetting(key: string, value: any): Promise<void> { - await apiFetch('/api/v1/user-settings', { - method: 'PUT', - body: JSON.stringify({ key, value }), - }) -} - -// ---- Task control APIs ---- - -export async function sendTaskMessage(taskId: string, message: string): Promise<void> { - await apiFetch(`/api/v1/mesh/tasks/${taskId}/message`, { - method: 'POST', - body: JSON.stringify({ message }), - }) -} - -export async function stopTask(taskId: string): Promise<void> { - await apiFetch(`/api/v1/mesh/tasks/${taskId}/stop`, { - method: 'POST', - }) -} - -export async function continueTask(taskId: string): Promise<void> { - await apiFetch(`/api/v1/mesh/tasks/${taskId}/continue`, { - method: 'POST', - }) -} - -export async function startTask(taskId: string): Promise<void> { - await apiFetch(`/api/v1/mesh/tasks/${taskId}/start`, { - method: 'POST', - }) -} - -// ---- Contract interaction APIs ---- - -export async function sendContractMessage(taskId: string, message: string): Promise<void> { - await apiFetch(`/api/v1/mesh/tasks/${taskId}/message`, { - method: 'POST', - body: JSON.stringify({ message }), - }) -} - -export async function interruptContract(taskId: string): Promise<void> { - await apiFetch(`/api/v1/mesh/tasks/${taskId}/message`, { - method: 'POST', - body: JSON.stringify({ message: '/interrupt' }), - }) -} - -export async function getContractOutput(taskId: string): Promise<{ output: string }> { - const response = await apiFetch(`/api/v1/mesh/tasks/${taskId}/output`) - return response.json() -} diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts index 1853f4f..58f461c 100644 --- a/frontend/src/stores/index.ts +++ b/frontend/src/stores/index.ts @@ -1,29 +1,5 @@ import { atom } from 'nanostores' import { ChatMessage, Choice } from '../types' -import { upsertUserSetting } from '../services/directiveApi' - -// Document UI feature flag -export const documentUiEnabledStore = atom<boolean>( - (() => { - try { - const saved = localStorage.getItem('document_ui_enabled') - return saved === 'true' - } catch { - return false - } - })() -) - -export const setDocumentUiEnabled = async (enabled: boolean) => { - documentUiEnabledStore.set(enabled) - localStorage.setItem('document_ui_enabled', enabled.toString()) - // Persist to backend (best-effort) - try { - await upsertUserSetting('document_ui_enabled', enabled) - } catch (err) { - console.error('Failed to persist document UI setting:', err) - } -} // Authentication state export const isLoggedInStore = atom<boolean>(false) @@ -53,14 +29,6 @@ export const nameStore = atom<string>('???') export const backgroundStore = atom<string>('/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png') export const locationStore = atom<string>('Tokyo') export const configModalOpenStore = atom<boolean>(false) - -// Feature flags -export const documentEditorEnabledStore = atom<boolean>( - (() => { - const saved = localStorage.getItem('documentEditorEnabled') - return saved === 'true' - })() -) export const skipIntroStore = atom<boolean>( (() => { const saved = localStorage.getItem('skipIntro') @@ -118,11 +86,6 @@ export const setSkipIntro = (skip: boolean) => { localStorage.setItem('skipIntro', skip.toString()) } -export const setDocumentEditorEnabled = (enabled: boolean) => { - documentEditorEnabledStore.set(enabled) - localStorage.setItem('documentEditorEnabled', enabled.toString()) -} - export const setLoadingComplete = (complete: boolean) => { loadingCompleteStore.set(complete) if (complete) { diff --git a/frontend/src/styles/mobile.css b/frontend/src/styles/mobile.css index 7d1f282..c1c524d 100644 --- a/frontend/src/styles/mobile.css +++ b/frontend/src/styles/mobile.css @@ -51,112 +51,4 @@ .makima-inline-icon { width: 16px; height: 16px; margin-left: 6px; } .makima-list { gap: 4px; } .makima-list li { font-size: 12px; line-height: 1.45; } - - /* ================== Professional Landing Page – Mobile ================== */ - - /* Compact header */ - .pro-header-content { - padding: 0.45rem 1rem; - } - - .pro-company-name { - font-size: 0.85rem; - letter-spacing: 0.12em; - } - - .pro-crane-logo { - height: 28px; - } - - /* Hide center heart on small screens to save space */ - .pro-header-center { - display: none; - } - - /* Full-width nav buttons */ - .pro-header-nav { - gap: 0.15rem; - } - - .pro-nav-link { - font-size: 0.7rem; - padding: 0.3rem 0.5rem; - } - - .pro-nav-login { - margin-left: 0.25rem; - } - - /* Hero – reduce heading size and padding */ - .pro-hero { - min-height: 85vh; - padding: 5rem 1.25rem 3rem; - } - - .pro-hero-headline { - font-size: clamp(1.4rem, 5.5vw, 2rem); - } - - .pro-hero-sub { - font-size: 0.9rem; - margin-bottom: 2rem; - } - - .pro-hero-tagline-jp { - font-size: 0.75rem; - letter-spacing: 0.35em; - } - - .pro-hero-cta { - flex-direction: column; - align-items: stretch; - } - - .pro-btn-primary, - .pro-btn-secondary { - width: 100%; - text-align: center; - justify-content: center; - } - - /* Stack cards to single column */ - .pro-content-grid { - grid-template-columns: 1fr; - padding: 0 1rem 3rem; - gap: 1.5rem; - } - - .pro-card-header { - padding: 1rem 1.25rem 0.6rem; - } - - .pro-card-body { - padding: 0.6rem 1.25rem 1.25rem; - } - - .pro-card-title { - font-size: 1rem; - } - - .pro-card-subtitle { - font-size: 0.88rem; - } - - .pro-card-text { - font-size: 0.8rem; - } - - .pro-makima-logo { - width: 48px; - height: 48px; - } - - /* Footer compact */ - .pro-footer { - padding: 1.25rem 1rem; - } - - .pro-footer-inner { - font-size: 0.7rem; - } } diff --git a/frontend/src/styles/pc98.css b/frontend/src/styles/pc98.css index e591cdc..7ec0d1c 100644 --- a/frontend/src/styles/pc98.css +++ b/frontend/src/styles/pc98.css @@ -4680,326 +4680,3 @@ button:focus-visible { .capacity-fill.high { background: #ffcc66; } .capacity-fill.full { background: #ff4466; } .refresh-indicator { font-size: 11px; color: rgba(255, 255, 255, 0.3); margin-left: auto; } - -/* ================== Professional Landing Page ================== */ - -/* ── Header ── */ - -.pro-header { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 100; - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - background: rgba(10, 14, 28, 0.85); - border-bottom: 1px solid rgba(0, 200, 255, 0.15); -} - -.pro-header-content { - max-width: 1100px; - margin: 0 auto; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.6rem 1.5rem; -} - -.pro-header-left { - display: flex; - align-items: center; - gap: 0.65rem; -} - -.pro-crane-logo { - display: block; -} - -.pro-company-name { - font-family: var(--font-family, 'IBM Plex Mono', 'MS Gothic', monospace); - font-size: 1rem; - font-weight: 600; - letter-spacing: 0.18em; - color: #e0e8f0; -} - -.pro-header-center { - display: flex; - align-items: center; - justify-content: center; -} - -.pro-header-nav { - display: flex; - align-items: center; - gap: 0.25rem; -} - -.pro-nav-link { - background: none; - border: 1px solid transparent; - color: #8fa8c8; - font-family: var(--font-family, 'IBM Plex Mono', 'MS Gothic', monospace); - font-size: 0.8rem; - letter-spacing: 0.08em; - padding: 0.35rem 0.75rem; - cursor: pointer; - border-radius: 3px; - transition: color 0.2s, border-color 0.2s; -} - -.pro-nav-link:hover { - color: #00c8ff; - border-color: rgba(0, 200, 255, 0.3); -} - -.pro-nav-login { - color: #00c8ff; - border-color: rgba(0, 200, 255, 0.35); - margin-left: 0.5rem; -} - -.pro-nav-login:hover { - background: rgba(0, 200, 255, 0.08); - border-color: #00c8ff; -} - -/* ── Landing Container ── */ - -.pro-landing { - min-height: 100vh; - background: #080c1a; - color: #c8d4e0; - font-family: var(--font-family, 'IBM Plex Mono', 'MS Gothic', monospace); - overflow-x: hidden; -} - -/* ── Hero ── */ - -.pro-hero { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - padding: 6rem 1.5rem 4rem; - text-align: center; - position: relative; -} - -.pro-hero::before { - content: ''; - position: absolute; - inset: 0; - background: - radial-gradient(ellipse 60% 50% at 50% 40%, rgba(0, 100, 180, 0.08) 0%, transparent 100%), - radial-gradient(ellipse 40% 30% at 50% 60%, rgba(0, 200, 255, 0.04) 0%, transparent 100%); - pointer-events: none; -} - -.pro-hero-inner { - position: relative; - max-width: 700px; -} - -.pro-hero-tagline-jp { - font-size: 0.85rem; - letter-spacing: 0.5em; - color: rgba(0, 200, 255, 0.45); - margin-bottom: 1.5rem; - text-transform: uppercase; -} - -.pro-hero-headline { - font-size: clamp(1.8rem, 4vw, 2.8rem); - font-weight: 600; - line-height: 1.25; - color: #e8f0fa; - margin: 0 0 1.25rem; - letter-spacing: -0.01em; -} - -.pro-hero-sub { - font-size: 1rem; - line-height: 1.7; - color: #8fa8c8; - margin: 0 auto 2.5rem; - max-width: 520px; -} - -.pro-hero-cta { - display: flex; - gap: 1rem; - justify-content: center; - flex-wrap: wrap; -} - -.pro-btn-primary, -.pro-btn-secondary { - font-family: var(--font-family, 'IBM Plex Mono', 'MS Gothic', monospace); - font-size: 0.85rem; - letter-spacing: 0.06em; - padding: 0.65rem 1.5rem; - border-radius: 3px; - cursor: pointer; - transition: all 0.2s; -} - -.pro-btn-primary { - background: rgba(0, 200, 255, 0.1); - color: #00c8ff; - border: 1px solid rgba(0, 200, 255, 0.35); -} - -.pro-btn-primary:hover { - background: rgba(0, 200, 255, 0.18); - border-color: #00c8ff; -} - -.pro-btn-secondary { - background: transparent; - color: #8fa8c8; - border: 1px solid rgba(140, 170, 200, 0.25); -} - -.pro-btn-secondary:hover { - color: #e0e8f0; - border-color: rgba(140, 170, 200, 0.5); -} - -.pro-btn-icon { - margin-right: 0.35rem; - font-size: 0.75rem; -} - -/* ── Content Grid ── */ - -.pro-content-grid { - max-width: 1100px; - margin: 0 auto; - padding: 0 1.5rem 5rem; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 440px), 1fr)); - gap: 2rem; -} - -.pro-card { - background: rgba(12, 18, 36, 0.7); - border: 1px solid rgba(0, 200, 255, 0.12); - border-radius: 4px; - overflow: hidden; - scroll-margin-top: 5rem; - transition: border-color 0.3s ease, box-shadow 0.3s ease; -} - -.pro-card:hover { - border-color: rgba(0, 200, 255, 0.28); - box-shadow: 0 0 20px rgba(0, 200, 255, 0.06); -} - -.pro-card-header { - padding: 1.25rem 1.5rem 0.75rem; - display: flex; - align-items: center; - gap: 0.75rem; - flex-wrap: wrap; -} - -.pro-card-title { - font-size: 1.1rem; - font-weight: 600; - letter-spacing: 0.12em; - color: #e0e8f0; - margin: 0; - text-transform: uppercase; -} - -.pro-card-badge { - font-size: 0.65rem; - letter-spacing: 0.1em; - text-transform: uppercase; - padding: 0.2rem 0.5rem; - background: rgba(0, 200, 255, 0.1); - border: 1px solid rgba(0, 200, 255, 0.25); - border-radius: 2px; - color: #00c8ff; -} - -.pro-card-accent { - flex: 1; - height: 1px; - background: linear-gradient(90deg, rgba(0, 200, 255, 0.3), transparent); -} - -.pro-card-body { - padding: 0.75rem 1.5rem 1.5rem; -} - -.pro-card-subtitle { - font-size: 0.95rem; - font-weight: 500; - color: #c0d0e0; - margin: 0 0 0.75rem; - line-height: 1.5; -} - -.pro-card-text { - font-size: 0.85rem; - line-height: 1.7; - color: #7a90a8; - margin: 0 0 0.6rem; -} - -.pro-card-text:last-child { - margin-bottom: 0; -} - -.pro-makima-logo { - display: block; - width: 64px; - height: 64px; - margin-bottom: 1rem; - opacity: 0.75; -} - -/* ── Footer ── */ - -.pro-footer { - border-top: 1px solid rgba(0, 200, 255, 0.1); - padding: 1.5rem; - text-align: center; -} - -.pro-footer-inner { - font-size: 0.75rem; - color: #4a6080; - letter-spacing: 0.08em; -} - -.pro-footer-brand { - letter-spacing: 0.18em; - color: #5a7898; -} - -.pro-footer-sep { - margin: 0 0.5rem; - opacity: 0.4; -} - -.pro-footer-text { - color: #4a6080; -} - -/* ── Shared transitions ── */ - -.pro-landing.hidden, -.pro-header.hidden { - opacity: 0; - pointer-events: none; -} - -.pro-landing.fade-in, -.pro-header.fade-in { - opacity: 1; - transition: opacity 0.8s ease; -} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index b4f8fee..79408dc 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/bottombar.tsx","./src/components/choicemenu.tsx","./src/components/cityscapebackground.tsx","./src/components/configmodal.tsx","./src/components/contractdetail.tsx","./src/components/contractlist.tsx","./src/components/daemondetail.tsx","./src/components/daemonlist.tsx","./src/components/dialoguebox.tsx","./src/components/filedetail.tsx","./src/components/heartlogo.tsx","./src/components/landingpage.tsx","./src/components/loadingscreen.tsx","./src/components/origamidragonlogo.tsx","./src/components/topbar.tsx","./src/components/vnapp.tsx","./src/components/vninterface.tsx","./src/components/vnviewport.tsx","./src/components/document/autosaveplugin.tsx","./src/components/document/contextmenu.tsx","./src/components/document/directivefiletree.tsx","./src/components/document/documenteditor.tsx","./src/components/document/documentlayout.tsx","./src/components/document/documentsettings.tsx","./src/components/document/editortheme.ts","./src/components/document/toast.tsx","./src/components/document/index.ts","./src/components/document/nodes/contractblockcomponent.tsx","./src/components/document/nodes/contractblocknode.tsx","./src/components/document/nodes/contractlogfeed.tsx","./src/components/document/nodes/steplogfeed.tsx","./src/components/document/nodes/stepsdiagramcomponent.tsx","./src/components/document/nodes/stepsdiagramnode.tsx","./src/services/directiveapi.ts","./src/services/taskws.ts","./src/services/ws.ts","./src/stores/index.ts"],"version":"5.9.2"}
\ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/bottombar.tsx","./src/components/choicemenu.tsx","./src/components/cityscapebackground.tsx","./src/components/configmodal.tsx","./src/components/contractdetail.tsx","./src/components/contractlist.tsx","./src/components/dialoguebox.tsx","./src/components/filedetail.tsx","./src/components/heartlogo.tsx","./src/components/landingpage.tsx","./src/components/loadingscreen.tsx","./src/components/origamidragonlogo.tsx","./src/components/topbar.tsx","./src/components/vnapp.tsx","./src/components/vninterface.tsx","./src/components/vnviewport.tsx","./src/services/ws.ts","./src/stores/index.ts"],"version":"5.9.2"}
\ No newline at end of file diff --git a/makima/frontend/src/components/directives/TaskSlideOutPanel.tsx b/makima/frontend/src/components/directives/TaskSlideOutPanel.tsx index 176728c..f803f90 100644 --- a/makima/frontend/src/components/directives/TaskSlideOutPanel.tsx +++ b/makima/frontend/src/components/directives/TaskSlideOutPanel.tsx @@ -25,6 +25,8 @@ export function TaskSlideOutPanel({ const [showDiff, setShowDiff] = useState(false); const [diffContent, setDiffContent] = useState<string>(""); const [diffLoading, setDiffLoading] = useState(false); + const [selectedFileDiff, setSelectedFileDiff] = useState<string | null>(null); + const [selectedFilePath, setSelectedFilePath] = useState<string | null>(null); // Escape key handler useEffect(() => { @@ -254,10 +256,12 @@ export function TaskSlideOutPanel({ )} </div> - {/* Worktree Changes section (~40% height) */} - <div className="flex-[2] min-h-0 overflow-y-auto"> - {taskId && <WorktreeFilesPanel taskId={taskId} onFileClick={handleFileClick} />} - </div> + {/* Worktree Changes section (~40% height) */} + <div className="flex-[2] min-h-0 overflow-y-auto"> + {taskId && <WorktreeFilesPanel taskId={taskId} onFileClick={handleFileClick} />} + </div> + </> + )} </div> </div> diff --git a/makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx b/makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx index bb3361d..7be39da 100644 --- a/makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx +++ b/makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx @@ -117,7 +117,6 @@ export function WorktreeFilesPanel({ taskId, onFileClick }: WorktreeFilesPanelPr const { stats, files } = worktreeInfo; const displayFiles = expanded ? files : files.slice(0, 10); - const isClickable = !!onFileClick; return ( <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)]"> diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index aecdac7..d597b44 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3078,6 +3078,15 @@ export async function getTaskDiff(taskId: string): Promise<{ taskId: string; suc return res.json(); } +/** Get the worktree diff for a task, optionally scoped to a single file. */ +export async function getWorktreeDiff( + taskId: string, + _filePath?: string, +): Promise<{ diff: string }> { + const result = await getTaskDiff(taskId); + return { diff: result.diff ?? "" }; +} + /** Commit changes in a task's worktree */ export async function commitWorktree(taskId: string, message?: string): Promise<{ taskId: string; success: boolean; commitSha: string | null; error: string | null }> { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/worktree-commit`, { diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index c4183f3..df3e8e7 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -864,6 +864,128 @@ async fn run_directive( let result = client.create_order(&req).await?; println!("{}", serde_json::to_string(&result.0)?); } + DirectiveCommand::Verify(args) => { + run_directive_verify(args).await?; + } + } + + Ok(()) +} + +/// Run `makima directive verify` — checks that the current HEAD merges cleanly +/// into `<remote>/<base>`. Prints a JSON result and exits non-zero on conflict. +/// +/// Implementation uses `git merge-tree --write-tree` (Git ≥ 2.38), which performs +/// the merge in-memory and lists conflicting paths without touching the working +/// tree or creating any commits. +async fn run_directive_verify( + args: makima::daemon::cli::directive::VerifyArgs, +) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + use std::process::Command; + + fn git(args: &[&str]) -> std::io::Result<std::process::Output> { + Command::new("git").args(args).output() + } + + let head_ref = args.head.as_deref().unwrap_or("HEAD").to_string(); + let base_ref = format!("{}/{}", args.remote, args.base); + + if !args.skip_fetch { + eprintln!("Fetching {} {}...", args.remote, args.base); + let fetch = git(&["fetch", &args.remote, &args.base])?; + if !fetch.status.success() { + return Err(format!( + "git fetch {} {} failed: {}", + args.remote, + args.base, + String::from_utf8_lossy(&fetch.stderr) + ) + .into()); + } + } + + let head_rev = { + let out = git(&["rev-parse", &head_ref])?; + if !out.status.success() { + return Err(format!( + "git rev-parse {} failed: {}", + head_ref, + String::from_utf8_lossy(&out.stderr) + ) + .into()); + } + String::from_utf8_lossy(&out.stdout).trim().to_string() + }; + let base_rev = { + let out = git(&["rev-parse", &base_ref])?; + if !out.status.success() { + return Err(format!( + "git rev-parse {} failed (did you fetch?): {}", + base_ref, + String::from_utf8_lossy(&out.stderr) + ) + .into()); + } + String::from_utf8_lossy(&out.stdout).trim().to_string() + }; + + eprintln!("Verifying merge: {} ({}) <- {} ({})", base_ref, &base_rev[..7.min(base_rev.len())], head_ref, &head_rev[..7.min(head_rev.len())]); + + let merge = Command::new("git") + .args(["merge-tree", "--write-tree", "--name-only", "--no-messages", &base_rev, &head_rev]) + .output()?; + + let stdout = String::from_utf8_lossy(&merge.stdout).to_string(); + let stderr = String::from_utf8_lossy(&merge.stderr).to_string(); + let success = merge.status.success(); + + let conflicting_files: Vec<String> = if success { + Vec::new() + } else { + stdout + .lines() + .skip(1) + .filter(|l| !l.is_empty()) + .map(|l| l.to_string()) + .collect() + }; + + let result = serde_json::json!({ + "ok": success, + "base": base_ref, + "head": head_ref, + "baseSha": base_rev, + "headSha": head_rev, + "conflictingFiles": conflicting_files, + "goal": args.goal, + }); + println!("{}", serde_json::to_string(&result)?); + + if !success { + eprintln!("\n[FAIL] Merge would conflict in {} file(s):", conflicting_files.len()); + for f in &conflicting_files { + eprintln!(" - {}", f); + } + if !stderr.is_empty() { + eprintln!("\ngit stderr:\n{}", stderr); + } + eprintln!( + "\nFix the conflicts before pushing. Typical workflow:\n \ + git fetch {remote} {base}\n \ + git merge {remote}/{base}\n \ + # resolve conflicts, commit, then re-run `makima directive verify`", + remote = args.remote, + base = args.base, + ); + std::process::exit(1); + } + + if let Some(goal) = &args.goal { + eprintln!("\n[OK] No merge conflicts."); + eprintln!("Reminder — directive goal:\n {}\n", goal); + eprintln!("Confirm the diff (`git diff {}...HEAD`) actually delivers this goal before creating the PR.", base_ref); + } else { + eprintln!("[OK] No merge conflicts with {}.", base_ref); } Ok(()) diff --git a/makima/src/daemon/cli/directive.rs b/makima/src/daemon/cli/directive.rs index cc7b224..0f04720 100644 --- a/makima/src/daemon/cli/directive.rs +++ b/makima/src/daemon/cli/directive.rs @@ -177,6 +177,32 @@ pub struct CreateOrderArgs { pub labels: Option<String>, } +/// Arguments for verify command — checks the current worktree can merge into the +/// directive's base branch with no conflicts. Runs entirely locally; no API call. +#[derive(Args, Debug)] +pub struct VerifyArgs { + /// Base branch to attempt merging into (e.g., "master", "main"). + #[arg(long, default_value = "master")] + pub base: String, + + /// Remote name to fetch from (default: "origin"). + #[arg(long, default_value = "origin")] + pub remote: String, + + /// Head ref to verify (default: current HEAD). + #[arg(long)] + pub head: Option<String>, + + /// Skip the `git fetch` step (use already-fetched remote ref). + #[arg(long, default_value = "false")] + pub skip_fetch: bool, + + /// Optional directive goal text. When provided the goal is echoed back as a + /// reminder so the calling orchestrator can self-check goal alignment. + #[arg(long)] + pub goal: Option<String>, +} + /// Arguments for update command. #[derive(Args, Debug)] pub struct UpdateArgs { diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index af6f885..7affc55 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -255,6 +255,13 @@ pub enum DirectiveCommand { /// Create an order for future work (spike or chore only) CreateOrder(directive::CreateOrderArgs), + + /// Verify the current worktree merges cleanly into the directive's base branch. + /// + /// Mandatory pre-flight before creating or pushing a directive PR — fails + /// with a non-zero exit code (and a list of conflicting files) if the merge + /// would conflict with the base branch. + Verify(directive::VerifyArgs), } impl Cli { diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index c11150f..97657dc 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -3050,36 +3050,4 @@ pub struct DirectiveOrderGroupListResponse { pub total: i64, } -// ============================================================================= -// User Settings Types -// ============================================================================= - -/// A user setting (feature flag / preference) stored in the database. -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UserSetting { - pub id: Uuid, - pub owner_id: Uuid, - pub key: String, - #[sqlx(json)] - pub value: serde_json::Value, - pub created_at: DateTime<Utc>, - pub updated_at: DateTime<Utc>, -} - -/// Request to upsert (create or update) a user setting. -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UpsertUserSettingRequest { - pub key: String, - pub value: serde_json::Value, -} - -/// Response wrapping a list of user settings. -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UserSettingsResponse { - pub settings: Vec<UserSetting>, -} - diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 5a912e4..57e8a78 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -6698,88 +6698,3 @@ pub async fn get_available_orders_for_dog_pickup( .await } -// ============================================================================= -// User Settings -// ============================================================================= - -/// Get all settings for a user. -pub async fn get_user_settings( - pool: &PgPool, - owner_id: Uuid, -) -> Result<Vec<UserSetting>, sqlx::Error> { - sqlx::query_as::<_, UserSetting>( - r#" - SELECT id, owner_id, key, value, created_at, updated_at - FROM user_settings - WHERE owner_id = $1 - ORDER BY key ASC - "#, - ) - .bind(owner_id) - .fetch_all(pool) - .await -} - -/// Get a specific setting by key for a user. -pub async fn get_user_setting( - pool: &PgPool, - owner_id: Uuid, - key: &str, -) -> Result<Option<UserSetting>, sqlx::Error> { - sqlx::query_as::<_, UserSetting>( - r#" - SELECT id, owner_id, key, value, created_at, updated_at - FROM user_settings - WHERE owner_id = $1 AND key = $2 - "#, - ) - .bind(owner_id) - .bind(key) - .fetch_optional(pool) - .await -} - -/// Upsert (create or update) a user setting. -pub async fn upsert_user_setting( - pool: &PgPool, - owner_id: Uuid, - key: &str, - value: &serde_json::Value, -) -> Result<UserSetting, sqlx::Error> { - sqlx::query_as::<_, UserSetting>( - r#" - INSERT INTO user_settings (owner_id, key, value) - VALUES ($1, $2, $3) - ON CONFLICT (owner_id, key) DO UPDATE SET - value = EXCLUDED.value, - updated_at = NOW() - RETURNING id, owner_id, key, value, created_at, updated_at - "#, - ) - .bind(owner_id) - .bind(key) - .bind(value) - .fetch_one(pool) - .await -} - -/// Delete a user setting by key. Returns true if a row was deleted. -pub async fn delete_user_setting( - pool: &PgPool, - owner_id: Uuid, - key: &str, -) -> Result<bool, sqlx::Error> { - let result = sqlx::query( - r#" - DELETE FROM user_settings - WHERE owner_id = $1 AND key = $2 - "#, - ) - .bind(owner_id) - .bind(key) - .execute(pool) - .await?; - - Ok(result.rows_affected() > 0) -} - diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index 1e025c8..8b3ae7e 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -1604,11 +1604,21 @@ Because of this, you MUST chain steps using dependsOn whenever one step's work b If step B modifies files created/changed by step A, step B MUST list step A in its dependsOn — otherwise step B will start from a blank worktree and won't see step A's changes at all. -Guidelines: -- For sequential work, create a linear chain: step1 → step2 → step3 (each depends on the previous). -- Only omit dependsOn for truly independent steps that can start from a fresh checkout. -- Parallel steps that share no files can omit mutual dependencies, but if they both build on a prior step - they should BOTH list that prior step in dependsOn. +Guidelines (DAG SHAPE — READ CAREFULLY): +- DEFAULT TO STRICTLY LINEAR CHAINS: step1 → step2 → step3 → … each step depends on the previous one. + This is the right shape for almost every directive. A linear chain inherits each previous step's + worktree, so later steps can see and build on earlier work, and the final merge is just a fast-forward + with at most a rebase against the base branch. +- ONLY use parallel steps (same orderIndex, no mutual dependsOn) when the work is GENUINELY independent: + the steps modify completely disjoint files or modules AND neither needs the other's output. + Pure-frontend vs pure-backend work in separate folders is the prototypical example. If you can name + even one shared file or one shared concept, the steps must be sequential. +- WHEN IN DOUBT, MAKE IT SEQUENTIAL. The cost of unnecessary serialization is one extra step run. + The cost of unnecessary parallelism is merge conflicts, lost work, and a final PR that has to be + hand-reconciled — exactly the failure mode this rule exists to prevent. +- A directive with N parallel branches is suspicious; a directive with N+1 sequential steps is the norm. + If you find yourself drawing a diamond (A → {{B, C}} → D), strongly reconsider whether B and C are + actually independent or whether one should follow the other. IMPORTANT: Each step's taskPlan must be self-contained. The executing instance won't have your planning context. @@ -1709,6 +1719,11 @@ Goal: {goal} Steps completed: {step_summary} +NOTE: This directive was planned with the strict-linear-DAG rule, so the step branches +should generally merge cleanly. If a merge produces meaningful conflicts, that is a +signal the plan was wrong, not routine work — prefer asking for help over papering +over conflicts with `-X theirs`. + 1. Clear the old PR URL: ``` makima directive update --pr-url "" @@ -1720,7 +1735,6 @@ git fetch origin NEW_BRANCH="{directive_branch}-v$(date +%s)" git checkout -b "$NEW_BRANCH" origin/{base_branch} {merge_commands} -git push -u origin "$NEW_BRANCH" ``` For each step branch merge above, if a merge fails with conflicts: @@ -1728,7 +1742,47 @@ For each step branch merge above, if a merge fails with conflicts: 2. If that also fails, manually resolve the conflicts, `git add .`, and `git commit --no-edit` 3. Continue with remaining merges -3. Generate a descriptive PR title and create a new PR: +## Step 2.5: MANDATORY pre-push verification + +Before pushing or creating the PR, you MUST run all three of these checks. Skipping any +of them is a directive failure. + +a) Build check — make sure the combined branch compiles: + - Rust backend (if any backend files changed): `cd makima && cargo check` + - Frontend (if any frontend files changed): `cd makima/frontend && npm install && npx tsc --noEmit` + Fix any errors before continuing. Do NOT push broken code. + +b) Merge-conflict check — MANDATORY: +``` +makima directive verify --base {base_branch} +``` + This must exit 0. If it exits non-zero, the branch will not merge cleanly into + `{base_branch}` and the PR is not ready. Resolve by: +``` + git fetch origin {base_branch} + git merge origin/{base_branch} + # resolve any conflicts, then `git add . && git commit --no-edit` + makima directive verify --base {base_branch} +``` + Re-run until verify exits 0. Do NOT push, create a PR, or call `makima directive update` + until verify passes. + +c) Goal-alignment self-check: + Run `git diff origin/{base_branch}...HEAD --stat` and review the file list. Confirm + the diff actually delivers the directive goal: + + {goal} + + If the diff is missing work the goal requires, finish the work or call + `makima directive ask "<question>" --phaseguard` for guidance. Do NOT push a PR that + does not deliver the goal. + +3. Push the branch: +``` +git push -u origin "$NEW_BRANCH" +``` + +4. Generate a descriptive PR title and create a new PR: Based on the steps completed above, generate a descriptive PR title that summarizes the actual changes (not just the directive name "{title}"). The title should: - Be concise (under 72 characters) @@ -1741,13 +1795,13 @@ gh pr create --title "<YOUR_GENERATED_TITLE>" --body "{pr_body}" --head "$NEW_BR ``` Replace <YOUR_GENERATED_TITLE> with the concise descriptive title you generated. -4. Store the new PR URL: +5. Store the new PR URL: ``` makima directive update --pr-url "<URL_FROM_GH_PR_CREATE>" ``` Replace the URL with the actual PR URL from the `gh pr create` output. This step is CRITICAL. -5. Update the directive pr_branch to the new branch name: +6. Update the directive pr_branch to the new branch name: ``` makima directive update --pr-branch "$NEW_BRANCH" ``` @@ -1759,14 +1813,13 @@ The PR is still open. Merge new step branches into the existing PR branch. Steps completed: {step_summary} -Run these commands: +Run these commands to update the branch (note: do NOT push yet — verification comes first): ``` git fetch origin git checkout {directive_branch} git pull origin {directive_branch} git merge origin/{base_branch} --no-edit {merge_commands} -git push origin {directive_branch} ``` Already-merged branches will be a no-op. If a merge fails with conflicts: @@ -1774,6 +1827,33 @@ Already-merged branches will be a no-op. If a merge fails with conflicts: 2. If that also fails, manually resolve the conflicts, `git add .`, and `git commit --no-edit` 3. Continue with remaining merges +## MANDATORY pre-push verification (also applies to PR updates) + +Before `git push`, run all three checks. Skipping any of them is a directive failure. + +a) Build check — Rust: `cd makima && cargo check`. Frontend (if changed): + `cd makima/frontend && npm install && npx tsc --noEmit`. Do NOT push broken code. + +b) Merge-conflict check — MANDATORY: +``` +makima directive verify --base {base_branch} +``` + Must exit 0. If not, merge `origin/{base_branch}` in, resolve, commit, re-verify. + Do NOT push until verify passes. + +c) Goal-alignment self-check — review `git diff origin/{base_branch}...HEAD --stat` + and confirm it still delivers the directive goal: + + {goal} + + If the goal has drifted (e.g., new step branches changed scope), update the PR + description after pushing or call `makima directive ask` for guidance. + +Then push: +``` +git push origin {directive_branch} +``` + If you encounter issues you cannot resolve (e.g., persistent merge conflicts, PR update failures), you can ask for help: makima directive ask "Your question" --phaseguard "#, @@ -1800,17 +1880,65 @@ Goal: {goal} Steps completed: {step_summary} -Run these commands to create a combined branch and PR: +NOTE: This directive was planned with the strict-linear-DAG rule, so the step branches +should generally merge cleanly. If a merge produces meaningful conflicts, that is a +signal the plan was wrong, not routine work — prefer asking for help over papering +over conflicts with `-X theirs`. + +## Step 1: Build the combined branch (do NOT push yet) ``` git fetch origin git checkout -b {directive_branch} origin/{base_branch} {merge_commands} -git push -u origin {directive_branch} ``` -Then generate a descriptive PR title and create the PR: +For each step branch merge, if a merge fails with conflicts: +1. First try: `git merge --abort` then retry with `git merge <the-failing-branch> -X theirs --no-edit` +2. If that also fails, manually resolve the conflicts, `git add .`, and `git commit --no-edit` +3. Continue with remaining merges -Based on the steps completed above, generate a descriptive PR title that summarizes the actual changes (not just the directive name "{title}"). The title should: +## Step 2: MANDATORY pre-push verification + +Before pushing or creating the PR, you MUST run all three of these checks. Skipping any +of them is a directive failure. + +a) Build check — make sure the combined branch compiles: + - Rust backend (if any backend files changed): `cd makima && cargo check` + - Frontend (if any frontend files changed): `cd makima/frontend && npm install && npx tsc --noEmit` + Fix any errors before continuing. Do NOT push broken code. + +b) Merge-conflict check — MANDATORY: +``` +makima directive verify --base {base_branch} +``` + This must exit 0. If it exits non-zero, the branch will not merge cleanly into + `{base_branch}` and the PR is not ready. Resolve by: +``` + git fetch origin {base_branch} + git merge origin/{base_branch} + # resolve any conflicts, then `git add . && git commit --no-edit` + makima directive verify --base {base_branch} +``` + Re-run until verify exits 0. Do NOT push, create a PR, or call `makima directive update` + until verify passes. + +c) Goal-alignment self-check: + Run `git diff origin/{base_branch}...HEAD --stat` and review the file list. Confirm + the diff actually delivers the directive goal: + + {goal} + + If the diff is missing work the goal requires, finish the work or call + `makima directive ask "<question>" --phaseguard` for guidance. Do NOT push a PR that + does not deliver the goal. + +## Step 3: Push and create the PR +``` +git push -u origin {directive_branch} +``` + +Generate a descriptive PR title that summarizes the actual changes (not just the directive +name "{title}"). The title should: - Be concise (under 72 characters) - Describe WHAT was done, not just the project name - Use conventional commit style if appropriate (feat:, fix:, refactor:, etc.) @@ -1821,21 +1949,17 @@ gh pr create --title "<YOUR_GENERATED_TITLE>" --body "{pr_body}" --head {directi ``` Replace <YOUR_GENERATED_TITLE> with the concise descriptive title you generated. -IMPORTANT: After creating the PR, you MUST store the PR URL so the directive system can track it. +## Step 4: Store the PR URL (CRITICAL) + +After creating the PR, you MUST store the PR URL so the directive system can track it. -1. Run `gh pr create` as shown above and capture its output -2. The output will contain the PR URL (e.g., https://github.com/owner/repo/pull/123) -3. Then run this command to store the URL: +1. The `gh pr create` output contains the PR URL (e.g., https://github.com/owner/repo/pull/123) +2. Run: ``` makima directive update --pr-url "https://github.com/..." ``` Replace the URL with the actual PR URL from the `gh pr create` output. This step is CRITICAL — the PR will not be tracked by the directive system without it. -For each step branch merge, if a merge fails with conflicts: -1. First try: `git merge --abort` then retry with `git merge <the-failing-branch> -X theirs --no-edit` -2. If that also fails, manually resolve the conflicts, `git add .`, and `git commit --no-edit` -3. Continue with remaining merges - If you encounter issues you cannot resolve (e.g., persistent merge conflicts, PR creation failures), you can ask for help: makima directive ask "Your question" --phaseguard "#, @@ -2195,11 +2319,19 @@ Because of this, you MUST chain steps using dependsOn whenever one step's work b If step B modifies files created/changed by step A, step B MUST list step A in its dependsOn — otherwise step B will start from a blank worktree and won't see step A's changes at all. -Guidelines: -- For sequential work, create a linear chain: step1 → step2 → step3 (each depends on the previous). -- Only omit dependsOn for truly independent steps that can start from a fresh checkout. -- Parallel steps that share no files can omit mutual dependencies, but if they both build on a prior step - they should BOTH list that prior step in dependsOn. +Guidelines (DAG SHAPE — READ CAREFULLY): +- DEFAULT TO STRICTLY LINEAR CHAINS: step1 → step2 → step3 → … each step depends on the previous one. + This is the right shape for almost every directive. A linear chain inherits each previous step's + worktree, so later steps can see and build on earlier work, and the final merge is just a fast-forward + with at most a rebase against the base branch. +- ONLY use parallel steps (same orderIndex, no mutual dependsOn) when the work is GENUINELY independent: + the steps modify completely disjoint files or modules AND neither needs the other's output. + Pure-frontend vs pure-backend work in separate folders is the prototypical example. If you can name + even one shared file or one shared concept, the steps must be sequential. +- WHEN IN DOUBT, MAKE IT SEQUENTIAL. The cost of unnecessary serialization is one extra step run. + The cost of unnecessary parallelism is merge conflicts, lost work, and a final PR that has to be + hand-reconciled — exactly the failure mode this rule exists to prevent. +- A directive with N parallel branches is suspicious; a directive with N+1 sequential steps is the norm. IMPORTANT: Each step's taskPlan must be self-contained. The executing instance won't have your planning context. diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index 99dc1f3..b382f04 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -281,7 +281,7 @@ pub fn make_router(state: SharedState) -> Router { .route("/timeline", get(history::get_timeline)) // Contract type templates (built-in only) .route("/contract-types", get(templates::list_contract_types)) - // Settings endpoints (static routes first to avoid conflict with /settings/{key}) + // Settings endpoints .route( "/settings/repository-history", get(repository_history::list_repository_history), @@ -294,15 +294,6 @@ pub fn make_router(state: SharedState) -> Router { "/settings/repository-history/{id}", axum::routing::delete(repository_history::delete_repository_history), ) - // User settings (feature flags / preferences) - .route( - "/settings", - get(settings::list_settings).put(settings::upsert_setting), - ) - .route( - "/settings/{key}", - get(settings::get_setting).delete(settings::delete_setting), - ) .with_state(state); let swagger = SwaggerUi::new("/swagger-ui") diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1f59868..0000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "6c8cfabe-soryu-co-soryu---makima--add-contract-blocks--expa", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} |
