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
10 changes: 5 additions & 5 deletions packages/tui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@
"test": "vitest run"
},
"dependencies": {
"@skillkit/core": "workspace:*",
"@skillkit/agents": "workspace:*",
"@opentui/core": "^0.1.75",
"@opentui/react": "^0.1.75",
"react": "^19.0.0"
"@opentui/solid": "^0.1.75",
"@skillkit/agents": "workspace:*",
"@skillkit/core": "workspace:*",
"solid-js": "^1.9.0"
},
"devDependencies": {
"@types/node": "^22.10.5",
"@types/react": "^19.0.0",
"esbuild-plugin-solid": "^0.6.0",
"tsup": "^8.3.5",
"typescript": "^5.7.2",
"vitest": "^2.1.8"
Expand Down
154 changes: 89 additions & 65 deletions packages/tui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useState, useCallback, useEffect } from 'react';
import { useKeyboard } from '@opentui/react';
import { exec } from 'node:child_process';
import { createSignal, createEffect, onCleanup, Show, Switch, Match } from 'solid-js';
import { useKeyboard } from '@opentui/solid';
import { execFile } from 'node:child_process';
import { type Screen, NAV_KEYS } from './state/types.js';
import { Sidebar } from './components/Sidebar.js';
import { Splash } from './components/Splash.js';
import { Sidebar } from './components/Sidebar.js';
import { StatusBar } from './components/StatusBar.js';
import {
Home, Browse, Installed, Marketplace, Settings, Recommend,
Translate, Context, Memory, Team, Plugins, Methodology,
Expand All @@ -13,52 +14,67 @@ import {
const DOCS_URL = 'https://agenstskills.com/docs';

function openUrl(url: string): void {
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
exec(`${cmd} ${url}`);
if (process.platform === 'win32') {
execFile('cmd', ['/c', 'start', '', url]);
} else {
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
execFile(cmd, [url]);
}
}

interface AppProps {
onExit?: (code?: number) => void;
}

export function App({ onExit }: AppProps = {}) {
const [showSplash, setShowSplash] = useState(true);
const [screen, setScreen] = useState<Screen>('home');
const [dimensions, setDimensions] = useState({
export function App(props: AppProps) {
const [showSplash, setShowSplash] = createSignal(true);
const [currentScreen, setCurrentScreen] = createSignal<Screen>('home');
const [showSidebar, setShowSidebar] = createSignal(true);
const [dimensions, setDimensions] = createSignal({
cols: process.stdout.columns || 80,
rows: process.stdout.rows || 24,
});

useEffect(() => {
createEffect(() => {
const handleResize = () => {
setDimensions({
cols: process.stdout.columns || 80,
rows: process.stdout.rows || 24,
});
};
process.stdout.on('resize', handleResize);
return () => { process.stdout.off('resize', handleResize); };
}, []);
onCleanup(() => {
process.stdout.off('resize', handleResize);
});
});

const { cols, rows } = dimensions;
const showSidebar = cols >= 60;
const cols = () => dimensions().cols;
const rows = () => dimensions().rows;
const sidebarVisible = () => showSidebar() && cols() >= 80;
const sidebarWidth = () => {
if (!sidebarVisible()) return 0;
if (cols() >= 100) return 24;
return 18;
};
const statusBarHeight = 2;
const contentHeight = () => rows() - statusBarHeight;

const handleNavigate = useCallback((newScreen: Screen) => {
setScreen(newScreen);
}, []);
const handleNavigate = (newScreen: Screen) => {
setCurrentScreen(newScreen);
};

const handleSplashComplete = useCallback(() => {
const handleSplashComplete = () => {
setShowSplash(false);
}, []);
};

useKeyboard((key: { name?: string; ctrl?: boolean }) => {
if (showSplash) {
useKeyboard((key: { name?: string; ctrl?: boolean; sequence?: string }) => {
if (showSplash()) {
setShowSplash(false);
return;
}

if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
onExit ? onExit(0) : process.exit(0);
props.onExit ? props.onExit(0) : process.exit(0);
return;
}

Expand All @@ -67,59 +83,67 @@ export function App({ onExit }: AppProps = {}) {
return;
}

if (key.sequence === '\\') {
setShowSidebar((v) => !v);
return;
}

const targetScreen = NAV_KEYS[key.name || ''];
if (targetScreen) {
setScreen(targetScreen);
setCurrentScreen(targetScreen);
}

if (key.name === 'escape' && screen !== 'home') {
setScreen('home');
if (key.name === 'escape' && currentScreen() !== 'home') {
setCurrentScreen('home');
}
});

if (showSplash) {
return <Splash onComplete={handleSplashComplete} duration={3000} />;
}

const screenProps = {
const screenProps = () => ({
onNavigate: handleNavigate,
cols: cols - (showSidebar ? 20 : 0),
rows,
};

const renderScreen = () => {
switch (screen) {
case 'home': return <Home {...screenProps} />;
case 'browse': return <Browse {...screenProps} />;
case 'installed': return <Installed {...screenProps} />;
case 'marketplace': return <Marketplace {...screenProps} />;
case 'settings': return <Settings {...screenProps} />;
case 'recommend': return <Recommend {...screenProps} />;
case 'translate': return <Translate {...screenProps} />;
case 'context': return <Context {...screenProps} />;
case 'memory': return <Memory {...screenProps} />;
case 'team': return <Team {...screenProps} />;
case 'plugins': return <Plugins {...screenProps} />;
case 'methodology': return <Methodology {...screenProps} />;
case 'plan': return <Plan {...screenProps} />;
case 'workflow': return <Workflow {...screenProps} />;
case 'execute': return <Execute {...screenProps} />;
case 'history': return <History {...screenProps} />;
case 'sync': return <Sync {...screenProps} />;
case 'help': return <Help {...screenProps} />;
case 'mesh': return <Mesh {...screenProps} />;
case 'message': return <Message {...screenProps} />;
default: return <Home {...screenProps} />;
}
};
cols: Math.max(1, cols() - sidebarWidth() - 2),
rows: Math.max(1, contentHeight() - 1),
});

return (
<box flexDirection="row" height={rows}>
{showSidebar && <Sidebar screen={screen} onNavigate={handleNavigate} />}
<box flexDirection="column" flexGrow={1} marginLeft={showSidebar ? 1 : 0} paddingRight={1}>
{renderScreen()}
<Show when={!showSplash()} fallback={<Splash onComplete={handleSplashComplete} duration={3000} />}>
<box flexDirection="column" height={rows()}>
<box flexDirection="row" height={contentHeight()}>
<Show when={sidebarVisible()}>
<Sidebar
screen={currentScreen()}
onNavigate={handleNavigate}
/>
</Show>

<box flexDirection="column" flexGrow={1} paddingX={1}>
<Switch fallback={<Home {...screenProps()} />}>
<Match when={currentScreen() === 'home'}><Home {...screenProps()} /></Match>
<Match when={currentScreen() === 'browse'}><Browse {...screenProps()} /></Match>
<Match when={currentScreen() === 'installed'}><Installed {...screenProps()} /></Match>
<Match when={currentScreen() === 'marketplace'}><Marketplace {...screenProps()} /></Match>
<Match when={currentScreen() === 'settings'}><Settings {...screenProps()} /></Match>
<Match when={currentScreen() === 'recommend'}><Recommend {...screenProps()} /></Match>
<Match when={currentScreen() === 'translate'}><Translate {...screenProps()} /></Match>
<Match when={currentScreen() === 'context'}><Context {...screenProps()} /></Match>
<Match when={currentScreen() === 'memory'}><Memory {...screenProps()} /></Match>
<Match when={currentScreen() === 'team'}><Team {...screenProps()} /></Match>
<Match when={currentScreen() === 'plugins'}><Plugins {...screenProps()} /></Match>
<Match when={currentScreen() === 'methodology'}><Methodology {...screenProps()} /></Match>
<Match when={currentScreen() === 'plan'}><Plan {...screenProps()} /></Match>
<Match when={currentScreen() === 'workflow'}><Workflow {...screenProps()} /></Match>
<Match when={currentScreen() === 'execute'}><Execute {...screenProps()} /></Match>
<Match when={currentScreen() === 'history'}><History {...screenProps()} /></Match>
<Match when={currentScreen() === 'sync'}><Sync {...screenProps()} /></Match>
<Match when={currentScreen() === 'help'}><Help {...screenProps()} /></Match>
<Match when={currentScreen() === 'mesh'}><Mesh {...screenProps()} /></Match>
<Match when={currentScreen() === 'message'}><Message {...screenProps()} /></Match>
</Switch>
</box>
</box>

<StatusBar />
</box>
</box>
</Show>
);
}

Expand Down
91 changes: 46 additions & 45 deletions packages/tui/src/components/AgentGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,69 @@
/**
* AgentGrid Component
* Displays monochromatic agent logos in a grid layout
*/
import { Show, For, createMemo } from 'solid-js';
import { AGENT_LOGOS, type AgentLogo } from '../theme/symbols.js';
import { terminalColors } from '../theme/colors.js';

interface AgentGridProps {
/** Maximum number of agents to display */
maxVisible?: number;
/** Show status indicators for detected agents */
showStatus?: boolean;
/** Agent types that are detected/active */
detectedAgents?: string[];
/** Number of columns in the grid */
columns?: number;
}

export function AgentGrid({
maxVisible = 12,
showStatus = true,
detectedAgents = [],
columns = 4,
}: AgentGridProps) {
const safeColumns = Math.max(1, columns);
export function AgentGrid(props: AgentGridProps) {
const maxVisible = () => props.maxVisible ?? 12;
const showStatus = () => props.showStatus ?? true;
const detectedAgents = () => props.detectedAgents ?? [];
const columns = () => Math.max(1, props.columns ?? 4);

const allAgents = Object.entries(AGENT_LOGOS);
const visibleAgents = allAgents.slice(0, maxVisible);
const hiddenCount = allAgents.length - maxVisible;
const visibleAgents = () => allAgents.slice(0, maxVisible());
const hiddenCount = () => allAgents.length - maxVisible();

const rows: [string, AgentLogo][][] = [];
for (let i = 0; i < visibleAgents.length; i += safeColumns) {
rows.push(visibleAgents.slice(i, i + safeColumns));
}
const rows = createMemo((): [string, AgentLogo][][] => {
const result: [string, AgentLogo][][] = [];
const visible = visibleAgents();
for (let i = 0; i < visible.length; i += columns()) {
result.push(visible.slice(i, i + columns()));
}
return result;
});

return (
<box flexDirection="column">
<text fg={terminalColors.text}>
<b>Works with</b>
</text>
<text> </text>
{rows.map((row, rowIndex) => (
<box key={`row-${rowIndex}`} flexDirection="row">
{row.map(([agentType, agent]) => {
const isDetected = detectedAgents.includes(agentType);
const statusIcon = showStatus ? (isDetected ? '●' : '○') : '';
const fg = isDetected ? terminalColors.accent : terminalColors.text;
<For each={rows()}>
{(row, rowIndex) => (
<box flexDirection="row">
<For each={row}>
{([agentType, agent]) => {
const isDetected = () => detectedAgents().includes(agentType);
const statusIcon = () => (showStatus() ? (isDetected() ? '●' : '○') : '');
const fg = () => (isDetected() ? terminalColors.accent : terminalColors.text);

return (
<box key={agentType} width={18}>
<text fg={fg}>
{agent.icon} {agent.name}
{showStatus && (
<span fg={isDetected ? terminalColors.success : terminalColors.textMuted}>
{' '}{statusIcon}
</span>
)}
</text>
</box>
);
})}
</box>
))}
{hiddenCount > 0 && (
<text fg={terminalColors.textMuted}>+{hiddenCount} more agents</text>
)}
return (
<box width={18}>
<text fg={fg()}>
{agent.icon} {agent.name}
<Show when={showStatus()}>
<span fg={isDetected() ? terminalColors.success : terminalColors.textMuted}>
{' '}
{statusIcon()}
</span>
</Show>
</text>
</box>
);
}}
</For>
</box>
)}
</For>
<Show when={hiddenCount() > 0}>
<text fg={terminalColors.textMuted}>+{hiddenCount()} more agents</text>
</Show>
</box>
);
}
Loading