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