From e9aebe7f739744533ab56d8ed7ca9cb9891555da Mon Sep 17 00:00:00 2001 From: Vatsal Umrania Date: Wed, 10 Jun 2026 20:13:07 +0530 Subject: [PATCH] 03: routing and screen layout --- bun.lock | 16 ++++- packages/cli/package.json | 4 +- .../src/components/messages/bot-message.tsx | 27 +++++++++ .../src/components/messages/error-message.tsx | 36 +++++++++++ .../cli/src/components/messages/index.tsx | 3 + .../src/components/messages/user-message.tsx | 35 +++++++++++ packages/cli/src/components/session-shell.tsx | 59 +++++++++++++++++++ packages/cli/src/components/spinner.tsx | 8 +++ packages/cli/src/index.tsx | 59 ++++++++----------- packages/cli/src/layouts/root-layout.tsx | 22 +++++++ packages/cli/src/layouts/themed-root.tsx | 21 +++++++ packages/cli/src/providers/theme/index.tsx | 2 +- packages/cli/src/screens/home.tsx | 34 +++++++++++ packages/cli/src/screens/new-session.tsx | 32 ++++++++++ packages/cli/src/screens/sessions.tsx | 5 ++ 15 files changed, 326 insertions(+), 37 deletions(-) create mode 100644 packages/cli/src/components/messages/bot-message.tsx create mode 100644 packages/cli/src/components/messages/error-message.tsx create mode 100644 packages/cli/src/components/messages/index.tsx create mode 100644 packages/cli/src/components/messages/user-message.tsx create mode 100644 packages/cli/src/components/session-shell.tsx create mode 100644 packages/cli/src/components/spinner.tsx create mode 100644 packages/cli/src/layouts/root-layout.tsx create mode 100644 packages/cli/src/layouts/themed-root.tsx create mode 100644 packages/cli/src/screens/home.tsx create mode 100644 packages/cli/src/screens/new-session.tsx create mode 100644 packages/cli/src/screens/sessions.tsx diff --git a/bun.lock b/bun.lock index 938c67b..0cf9a88 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,9 @@ "dependencies": { "@opentui/core": "^0.2.10", "@opentui/react": "^0.2.10", + "opentui-spinner": "^0.0.7", "react": "^19.2.6", + "react-router": "^7.17.0", }, "devDependencies": { "@types/bun": "latest", @@ -22,8 +24,6 @@ }, }, "packages": { - "@voidcode/cli": ["@voidcode/cli@workspace:packages/cli"], - "@opentui/core": ["@opentui/core@0.2.14", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.14", "@opentui/core-darwin-x64": "0.2.14", "@opentui/core-linux-arm64": "0.2.14", "@opentui/core-linux-x64": "0.2.14", "@opentui/core-win32-arm64": "0.2.14", "@opentui/core-win32-x64": "0.2.14" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-17YCr3BqM9mhi/DdNVM+omgmrKQNIl0G5RzoaTFOHe4+OAhG+W3iooYi+WdsekJWSUOEwZqDRz0QBTZhOtgZsQ=="], "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iS4NZQkOKX2EP5rsNjDcU7inDLcKhPaSBn8ENjDXKx2smOh7p/rgM2qlEaiLI3njtL784QoF+nxTzSXbEI6+Jw=="], @@ -46,12 +46,18 @@ "@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="], + "@voidcode/cli": ["@voidcode/cli@workspace:packages/cli"], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], @@ -62,14 +68,20 @@ "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + "opentui-spinner": ["opentui-spinner@0.0.7", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="], + "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], "react-devtools-core": ["react-devtools-core@7.0.1", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw=="], "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], + "react-router": ["react-router@7.17.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], diff --git a/packages/cli/package.json b/packages/cli/package.json index 6a9da03..0458d36 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -16,6 +16,8 @@ "dependencies": { "@opentui/core": "^0.2.10", "@opentui/react": "^0.2.10", - "react": "^19.2.6" + "opentui-spinner": "^0.0.7", + "react": "^19.2.6", + "react-router": "^7.17.0" } } \ No newline at end of file diff --git a/packages/cli/src/components/messages/bot-message.tsx b/packages/cli/src/components/messages/bot-message.tsx new file mode 100644 index 0000000..8a13ca5 --- /dev/null +++ b/packages/cli/src/components/messages/bot-message.tsx @@ -0,0 +1,27 @@ +import { useTheme } from "../../providers/theme"; + +type Props = { + content: string; + model: string; +}; + +export function BotMessage({content, model} : Props){ + const { colors } = useTheme(); + + return ( + + + + {content} + + + + + + + {model} + + + + ) +} \ No newline at end of file diff --git a/packages/cli/src/components/messages/error-message.tsx b/packages/cli/src/components/messages/error-message.tsx new file mode 100644 index 0000000..ef7a92c --- /dev/null +++ b/packages/cli/src/components/messages/error-message.tsx @@ -0,0 +1,36 @@ +import { TextAttributes } from "@opentui/core"; +import { EmptyBorder } from "../border"; +import { useTheme } from "../../providers/theme"; + +type Props = { + message: string; +}; + +export function ErrorMessage({message} : Props){ + const { colors } = useTheme(); + + return ( + + + + {message} + + + + ) +} \ No newline at end of file diff --git a/packages/cli/src/components/messages/index.tsx b/packages/cli/src/components/messages/index.tsx new file mode 100644 index 0000000..ef14348 --- /dev/null +++ b/packages/cli/src/components/messages/index.tsx @@ -0,0 +1,3 @@ +export { ErrorMessage } from "./error-message"; +export { UserMessage } from "./user-message"; +export { BotMessage } from "./bot-message"; \ No newline at end of file diff --git a/packages/cli/src/components/messages/user-message.tsx b/packages/cli/src/components/messages/user-message.tsx new file mode 100644 index 0000000..ac89007 --- /dev/null +++ b/packages/cli/src/components/messages/user-message.tsx @@ -0,0 +1,35 @@ +import { EmptyBorder } from "../border"; +import { useTheme } from "../../providers/theme"; + +type Props = { + message: string; +}; + +export function UserMessage({message} : Props){ + const { colors } = useTheme(); + + return ( + + + + {message} + + + + ) +} \ No newline at end of file diff --git a/packages/cli/src/components/session-shell.tsx b/packages/cli/src/components/session-shell.tsx new file mode 100644 index 0000000..99f3204 --- /dev/null +++ b/packages/cli/src/components/session-shell.tsx @@ -0,0 +1,59 @@ +import { TextAttributes } from "@opentui/core"; +import type { ReactNode } from "react"; +import { InputBar } from "./input-bar"; +import { Spinner } from "./spinner"; + +type Props = { + children?: ReactNode; + onSubmit: (text: string) => void; + inputDisabled?: boolean; + loading?: boolean; +}; + +export function SessionShell({ + children, + onSubmit, + inputDisabled = false, + loading = false, +}: Props) { + return ( + + + {children} + + + + + + + {loading ? : null} + + + + tab + agents + + + + ); +}; \ No newline at end of file diff --git a/packages/cli/src/components/spinner.tsx b/packages/cli/src/components/spinner.tsx new file mode 100644 index 0000000..43f49a6 --- /dev/null +++ b/packages/cli/src/components/spinner.tsx @@ -0,0 +1,8 @@ +import "opentui-spinner/react"; +import { useTheme } from "../providers/theme"; + +export function Spinner(){ + const { colors } = useTheme(); + + return +}; \ No newline at end of file diff --git a/packages/cli/src/index.tsx b/packages/cli/src/index.tsx index 8ca5266..6df1e18 100644 --- a/packages/cli/src/index.tsx +++ b/packages/cli/src/index.tsx @@ -1,42 +1,35 @@ import { createCliRenderer } from "@opentui/core"; import { createRoot } from "@opentui/react"; -import { Header } from "./components/header"; -import { InputBar } from "./components/input-bar"; -import { ToastProvider } from "./providers/toast"; -import { KeyboardLayerProvider } from "./providers/keyboard-layer"; -import { DialogProvider } from "./providers/dialog"; -import { ThemeProvider, useTheme } from "./providers/theme"; +import { createMemoryRouter, RouterProvider } from "react-router"; +import { RootLayout } from "./layouts/root-layout"; +import { Home } from "./screens/home"; +import { NewSession } from "./screens/new-session"; +import { Session } from "./screens/sessions"; -function ThemedRoot(){ - const { colors } = useTheme(); +const router = createMemoryRouter([ + { + path: "/", + element: , + children: [ + { + index: true, + element: + }, + { + path: "session/new", + element: + }, + { + path: "session/:id", + element: + }, + ] + } +]) - return ( - -
- - {}} /> - - - ) -} function App() { return ( - - - - - - - - - + ); } diff --git a/packages/cli/src/layouts/root-layout.tsx b/packages/cli/src/layouts/root-layout.tsx new file mode 100644 index 0000000..6b15ea4 --- /dev/null +++ b/packages/cli/src/layouts/root-layout.tsx @@ -0,0 +1,22 @@ +import { Outlet } from "react-router"; +import { ToastProvider } from "../providers/toast"; +import { DialogProvider } from "../providers/dialog"; +import { KeyboardLayerProvider } from "../providers/keyboard-layer"; +import { ThemeProvider } from "../providers/theme"; +import { ThemedRoot } from "./themed-root"; + +export function RootLayout(){ + return ( + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/packages/cli/src/layouts/themed-root.tsx b/packages/cli/src/layouts/themed-root.tsx new file mode 100644 index 0000000..fbe1512 --- /dev/null +++ b/packages/cli/src/layouts/themed-root.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from "react"; +import { useTheme } from "../providers/theme"; + +type Props = { + children: ReactNode; +}; + +export function ThemedRoot({children} : Props){ + const { colors } = useTheme(); + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/packages/cli/src/providers/theme/index.tsx b/packages/cli/src/providers/theme/index.tsx index 76662ab..e5298a8 100644 --- a/packages/cli/src/providers/theme/index.tsx +++ b/packages/cli/src/providers/theme/index.tsx @@ -6,7 +6,7 @@ import type { ReactNode } from "react"; import type { ThemeColors, Theme } from "../../theme"; import { DEFAULT_THEME, THEMES } from "../../theme"; -const CONFIG_DIR = join(homedir(), ".nightcode"); +const CONFIG_DIR = join(homedir(), ".voidcode"); const THEME_PREFERENCES_PATH = join(CONFIG_DIR, "preferences.json"); type ThemePreferences = { diff --git a/packages/cli/src/screens/home.tsx b/packages/cli/src/screens/home.tsx new file mode 100644 index 0000000..99fd7ca --- /dev/null +++ b/packages/cli/src/screens/home.tsx @@ -0,0 +1,34 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router"; +import { Header } from "../components/header"; +import { InputBar } from "../components/input-bar"; + +export function Home(){ + const navigate = useNavigate(); + const handleSubmit = useCallback( + (text: string) => { + navigate("/session/new", { + state: { message: text } + }) + }, + [navigate] + ); + + return ( + +
+ + + + + ); +}; + diff --git a/packages/cli/src/screens/new-session.tsx b/packages/cli/src/screens/new-session.tsx new file mode 100644 index 0000000..1e5672a --- /dev/null +++ b/packages/cli/src/screens/new-session.tsx @@ -0,0 +1,32 @@ +import { useEffect } from "react"; +import { useNavigate, useLocation } from "react-router"; +import { useTheme } from "../providers/theme"; +import { SessionShell } from "../components/session-shell"; +import { ErrorMessage, UserMessage, BotMessage } from "../components/messages"; + +export function NewSession() { + const navigate = useNavigate(); + const location = useLocation(); + const { colors } = useTheme(); + + const state = location.state as { message?: string } | null; + + useEffect(() => { + if(!state?.message){ + navigate("/", { replace: true }); + } + },[state, navigate]); + + if(!state?.message) return null; + + return ( + {}} inputDisabled loading> + + + + + ); +}; \ No newline at end of file diff --git a/packages/cli/src/screens/sessions.tsx b/packages/cli/src/screens/sessions.tsx new file mode 100644 index 0000000..9fe1fbb --- /dev/null +++ b/packages/cli/src/screens/sessions.tsx @@ -0,0 +1,5 @@ +import { SessionShell } from "../components/session-shell"; + +export function Session() { + return {}} inputDisabled loading/> +}; \ No newline at end of file