Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,34 @@
node_modules
# dependencies (bun install)
node_modules

# output
out
dist
*.tgz

# code coverage
coverage
*.lcov

# logs
logs
*.log
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# caches
.eslintcache
.cache
*.tsbuildinfo

# IntelliJ based IDEs
.idea

# Finder (MacOS) folder config
.DS_Store
91 changes: 91 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "nightcode",
"version": "1.0.0",
"workspaces": ["packages/*"],
"scripts": {
"dev:cli":"bun run --watch packages/cli/src/index.tsx"
}
}
21 changes: 21 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@nightcode/cli",
"module": "src/index.tsx",
"type": "module",
"private": true,
"scripts": {
"dev": "bun run --watch src/index.tsx"
},
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^19.2.15"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"@opentui/core": "^0.2.10",
"@opentui/react": "^0.2.10",
"react": "^19.2.6"
}
}
21 changes: 21 additions & 0 deletions packages/cli/src/components/border.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const EmptyBorder = {
topLeft: "",
bottomLeft: "",
vertical: "",
topRight: "",
bottomRight: "",
horizontal: " ",
bottomT: "",
topT: "",
cross: "",
leftT: "",
rightT: "",
}

export const SplitBorder = {
border: ["left" as const, "right" as const],
customBorderChars: {
...EmptyBorder,
vertical: "┃",
},
}
57 changes: 57 additions & 0 deletions packages/cli/src/components/command-menu/commands.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Command } from "./types";

export const COMMANDS: Command[] = [
{
name: "new",
description: "Start a new conversation",
value: "/new",
},
{
name: "agents",
description: "Switch agents",
value: "/agents",
},
{
name: "models",
description: "Select AI model for generation",
value: "/models",
},
{
name: "sessions",
description: "Browse past sessions",
value: "/sessions",
},
{
name: "theme",
description: "Change color theme",
value: "/theme",
},
{
name: "login",
description: "Sign in with your browser",
value: "/login",
},
{
name: "logout",
description: "Sign out of your account",
value: "/logout",
},
{
name: "upgrade",
description: "Buy more credits",
value: "/upgrade",
},
{
name: "usage",
description: "Open billing portal in your browser",
value: "/usage",
},
{
name: "exit",
description: "Quit the application",
value: "/exit",
action: (ctx) => {
ctx.exit();
},
},
];
8 changes: 8 additions & 0 deletions packages/cli/src/components/command-menu/filter-commands.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Command } from "./types";
import { COMMANDS } from "./commands";

export function getFilteredCommands(query: string): Command[] {
if (query.length === 0) return COMMANDS;
return COMMANDS
.filter((cmd) => cmd.name.toLowerCase().startsWith(query.toLowerCase()));
};
72 changes: 72 additions & 0 deletions packages/cli/src/components/command-menu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { RefObject } from "react";
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
import { getFilteredCommands } from "./filter-commands";
import { COMMANDS } from "./commands";

const MAX_VISIBLE_ITEMS = 8;

// Align all command names in a fixed-width column so their descriptions
// start at the same horizontal position for a clean tabular look.
// The width adjusts to accommodate the longest command name.
const COMMAND_COL_WIDTH = Math.max(...COMMANDS.map((cmd) => cmd.name.length)) + 4;

type CommandMenuProps = {
query: string;
selectedIndex: number;
scrollRef: RefObject<ScrollBoxRenderable | null>;
onSelect: (index: number) => void;
onExecute: (index: number) => void;
};

export function CommandMenu({
query,
selectedIndex,
scrollRef,
onSelect,
onExecute,
}: CommandMenuProps) {
const filtered = getFilteredCommands(query);
const visibleHeight = Math.min(filtered.length, MAX_VISIBLE_ITEMS);

if (filtered.length === 0) {
return (
<box paddingX={1}>
<text attributes={TextAttributes.DIM}>
No matching commands
</text>
</box>
);
}

return (
<scrollbox ref={scrollRef} height={visibleHeight}>
{filtered.map((cmd, i) => {
const isSelected = i === selectedIndex;

return (
<box
key={cmd.value}
flexDirection="row"
paddingX={1}
height={1}
overflow="hidden"
backgroundColor={isSelected ? "#89B4FA" : undefined}
onMouseMove={() => onSelect(i)}
onMouseDown={() => onExecute(i)}
>
<box width={COMMAND_COL_WIDTH} flexShrink={0}>
<text selectable={false} fg={isSelected ? "black" : "white"}>
/{cmd.name}
</text>
</box>
<box flexGrow={1} flexShrink={1} overflow="hidden">
<text selectable={false} fg={isSelected ? "black" : "gray"}>
{cmd.description}
</text>
</box>
</box>
);
})}
</scrollbox>
);
};
10 changes: 10 additions & 0 deletions packages/cli/src/components/command-menu/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type CommandContext = {
exit: () => void;
};

export type Command = {
name: string;
description: string;
value: string;
action?: (ctx: CommandContext) => void | Promise<void>;
};
Loading
Loading