Restructure src/ modules with consistent internal organization
Reorganize major src/ directories to follow a consistent pattern with
core/, menu/, submenu/, inputs/, logs/, layout/, feedback/ subdirectories.
Changes by module:
- stores/: Move 5 store files to stores/core/
- utils/: Create core/ (terminal, tools, etc.) and menu/ (progress-bar)
- api/: Create copilot/core/, copilot/auth/, ollama/core/
- providers/: Create core/, copilot/core/, copilot/auth/, ollama/core/, login/core/
- ui/: Create core/, banner/core/, banner/menu/, spinner/core/,
input-editor/core/, components/core/, components/menu/
- tools/: Create core/ for registry.ts and types.ts
- tui-solid/: Reorganize components/ into menu/, submenu/, inputs/,
logs/, modals/, panels/, layout/, feedback/
- commands/: Create core/ for runner.ts and handlers.ts
- services/: Create core/ for agent.ts, permissions.ts, session.ts,
executor.ts, config.ts
All imports updated to use new paths. TypeScript compilation verified.
This commit is contained in:
@@ -1,202 +0,0 @@
|
||||
import { For, createSignal, onMount, onCleanup } from "solid-js";
|
||||
import { useKeyboard } from "@opentui/solid";
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import type { ScrollBoxRenderable } from "@opentui/core";
|
||||
import { useTheme } from "@tui-solid/context/theme";
|
||||
import { useAppStore } from "@tui-solid/context/app";
|
||||
|
||||
const SCROLL_LINES = 2;
|
||||
|
||||
interface DebugEntry {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
type: "api" | "stream" | "tool" | "state" | "error" | "info" | "render";
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Global debug log store
|
||||
let debugEntries: DebugEntry[] = [];
|
||||
let debugIdCounter = 0;
|
||||
let listeners: Array<() => void> = [];
|
||||
|
||||
const notifyListeners = (): void => {
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
};
|
||||
|
||||
export const addDebugLog = (
|
||||
type: DebugEntry["type"],
|
||||
message: string,
|
||||
): void => {
|
||||
const entry: DebugEntry = {
|
||||
id: `debug-${++debugIdCounter}`,
|
||||
timestamp: Date.now(),
|
||||
type,
|
||||
message,
|
||||
};
|
||||
debugEntries.push(entry);
|
||||
// Keep only last 500 entries
|
||||
if (debugEntries.length > 500) {
|
||||
debugEntries = debugEntries.slice(-500);
|
||||
}
|
||||
notifyListeners();
|
||||
};
|
||||
|
||||
export const clearDebugLogs = (): void => {
|
||||
debugEntries = [];
|
||||
debugIdCounter = 0;
|
||||
notifyListeners();
|
||||
};
|
||||
|
||||
export function DebugLogPanel() {
|
||||
const theme = useTheme();
|
||||
const app = useAppStore();
|
||||
let scrollboxRef: ScrollBoxRenderable | undefined;
|
||||
const [entries, setEntries] = createSignal<DebugEntry[]>([...debugEntries]);
|
||||
const [stickyEnabled, setStickyEnabled] = createSignal(true);
|
||||
|
||||
const isActive = () => app.debugLogVisible();
|
||||
|
||||
onMount(() => {
|
||||
const updateEntries = (): void => {
|
||||
setEntries([...debugEntries]);
|
||||
if (stickyEnabled() && scrollboxRef) {
|
||||
scrollboxRef.scrollTo(Infinity);
|
||||
}
|
||||
};
|
||||
listeners.push(updateEntries);
|
||||
|
||||
onCleanup(() => {
|
||||
listeners = listeners.filter((l) => l !== updateEntries);
|
||||
});
|
||||
});
|
||||
|
||||
const getTypeColor = (type: DebugEntry["type"]): string => {
|
||||
const colorMap: Record<DebugEntry["type"], string> = {
|
||||
api: theme.colors.info,
|
||||
stream: theme.colors.success,
|
||||
tool: theme.colors.warning,
|
||||
state: theme.colors.accent,
|
||||
error: theme.colors.error,
|
||||
info: theme.colors.textDim,
|
||||
render: theme.colors.primary,
|
||||
};
|
||||
return colorMap[type];
|
||||
};
|
||||
|
||||
const getTypeLabel = (type: DebugEntry["type"]): string => {
|
||||
const labelMap: Record<DebugEntry["type"], string> = {
|
||||
api: "API",
|
||||
stream: "STR",
|
||||
tool: "TUL",
|
||||
state: "STA",
|
||||
error: "ERR",
|
||||
info: "INF",
|
||||
render: "RND",
|
||||
};
|
||||
return labelMap[type];
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: number): string => {
|
||||
const date = new Date(timestamp);
|
||||
return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const scrollUp = (): void => {
|
||||
if (!scrollboxRef) return;
|
||||
setStickyEnabled(false);
|
||||
scrollboxRef.scrollBy(-SCROLL_LINES);
|
||||
};
|
||||
|
||||
const scrollDown = (): void => {
|
||||
if (!scrollboxRef) return;
|
||||
scrollboxRef.scrollBy(SCROLL_LINES);
|
||||
|
||||
const isAtBottom =
|
||||
scrollboxRef.scrollTop >=
|
||||
scrollboxRef.content.height - scrollboxRef.viewport.height - 1;
|
||||
if (isAtBottom) {
|
||||
setStickyEnabled(true);
|
||||
}
|
||||
};
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (!isActive()) return;
|
||||
|
||||
if (evt.shift && evt.name === "pageup") {
|
||||
scrollUp();
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.shift && evt.name === "pagedown") {
|
||||
scrollDown();
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
const truncateMessage = (msg: string, maxLen: number): string => {
|
||||
if (msg.length <= maxLen) return msg;
|
||||
return msg.substring(0, maxLen - 3) + "...";
|
||||
};
|
||||
|
||||
return (
|
||||
<box
|
||||
flexDirection="column"
|
||||
width="20%"
|
||||
borderColor={theme.colors.border}
|
||||
border={["top", "bottom", "left", "right"]}
|
||||
backgroundColor={theme.colors.background}
|
||||
>
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
borderColor={theme.colors.border}
|
||||
border={["bottom"]}
|
||||
flexDirection="row"
|
||||
>
|
||||
<text fg={theme.colors.accent} attributes={TextAttributes.BOLD}>
|
||||
Debug Logs ({entries().length})
|
||||
</text>
|
||||
</box>
|
||||
|
||||
<scrollbox
|
||||
ref={scrollboxRef}
|
||||
stickyScroll={stickyEnabled()}
|
||||
stickyStart="bottom"
|
||||
flexGrow={1}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
>
|
||||
<box flexDirection="column">
|
||||
<For each={entries()}>
|
||||
{(entry) => (
|
||||
<box flexDirection="row">
|
||||
<text fg={theme.colors.textDim}>
|
||||
{formatTime(entry.timestamp)}{" "}
|
||||
</text>
|
||||
<text fg={getTypeColor(entry.type)}>
|
||||
[{getTypeLabel(entry.type)}]{" "}
|
||||
</text>
|
||||
<text fg={theme.colors.text} wrapMode="word">
|
||||
{truncateMessage(entry.message, 50)}
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</scrollbox>
|
||||
|
||||
<box
|
||||
paddingLeft={1}
|
||||
borderColor={theme.colors.border}
|
||||
border={["top"]}
|
||||
>
|
||||
<text fg={theme.colors.textDim}>Shift+PgUp/PgDn scroll</text>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
6
src/tui-solid/components/feedback/index.ts
Normal file
6
src/tui-solid/components/feedback/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Feedback Components
|
||||
*/
|
||||
|
||||
export * from "./thinking-indicator";
|
||||
export * from "./bouncing-loader";
|
||||
@@ -1,27 +1,47 @@
|
||||
export { StatusBar } from "./status-bar";
|
||||
export { Logo } from "./logo";
|
||||
export { ThinkingIndicator } from "./thinking-indicator";
|
||||
export { BouncingLoader } from "./bouncing-loader";
|
||||
export { LogPanel } from "./log-panel";
|
||||
export { LogEntryDisplay } from "./log-entry";
|
||||
export { StreamingMessage } from "./streaming-message";
|
||||
export { InputArea } from "./input-area";
|
||||
export { Header } from "./header";
|
||||
export { CommandMenu, SLASH_COMMANDS } from "./command-menu";
|
||||
export { ModelSelect } from "./model-select";
|
||||
export { AgentSelect } from "./agent-select";
|
||||
export { ThemeSelect } from "./theme-select";
|
||||
export { MCPSelect } from "./mcp-select";
|
||||
export { MCPAddForm } from "./mcp-add-form";
|
||||
export { ModeSelect } from "./mode-select";
|
||||
export { ProviderSelect } from "./provider-select";
|
||||
export { FilePicker } from "./file-picker";
|
||||
export { SelectMenu } from "./select-menu";
|
||||
export type { SelectOption } from "./select-menu";
|
||||
export { PermissionModal } from "./permission-modal";
|
||||
export { LearningModal } from "./learning-modal";
|
||||
export { HelpMenu } from "./help-menu";
|
||||
export { HelpDetail } from "./help-detail";
|
||||
export { TodoPanel } from "./todo-panel";
|
||||
export type { TodoItem, Plan } from "./todo-panel";
|
||||
export { DiffView, parseDiffOutput, isDiffContent } from "./diff-view";
|
||||
// Layout components
|
||||
export { StatusBar } from "./layout/status-bar";
|
||||
export { Logo } from "./layout/logo";
|
||||
export { StreamingMessage } from "./layout/streaming-message";
|
||||
export { Header } from "./layout/header";
|
||||
|
||||
// Feedback components
|
||||
export { ThinkingIndicator } from "./feedback/thinking-indicator";
|
||||
export { BouncingLoader } from "./feedback/bouncing-loader";
|
||||
|
||||
// Log components
|
||||
export { LogPanel } from "./logs/log-panel";
|
||||
export { LogEntryDisplay } from "./logs/log-entry";
|
||||
export { addDebugLog, DebugLogPanel } from "./logs/debug-log-panel";
|
||||
|
||||
// Input components
|
||||
export { InputArea } from "./inputs/input-area";
|
||||
export { FilePicker } from "./inputs/file-picker";
|
||||
export { MCPAddForm } from "./inputs/mcp-add-form";
|
||||
|
||||
// Menu components
|
||||
export { CommandMenu, SLASH_COMMANDS } from "./menu/command-menu";
|
||||
export { SelectMenu } from "./menu/select-menu";
|
||||
export type { SelectOption } from "./menu/select-menu";
|
||||
export { HelpMenu } from "./menu/help-menu";
|
||||
export { BrainMenu } from "./menu/brain-menu";
|
||||
|
||||
// Submenu components
|
||||
export { ModelSelect } from "./submenu/model-select";
|
||||
export { AgentSelect } from "./submenu/agent-select";
|
||||
export { ThemeSelect } from "./submenu/theme-select";
|
||||
export { MCPSelect } from "./submenu/mcp-select";
|
||||
export { ModeSelect } from "./submenu/mode-select";
|
||||
export { ProviderSelect } from "./submenu/provider-select";
|
||||
|
||||
// Modal components
|
||||
export { PermissionModal } from "./modals/permission-modal";
|
||||
export { LearningModal } from "./modals/learning-modal";
|
||||
export { CenteredModal } from "./modals/centered-modal";
|
||||
export { ConflictResolver, ConflictIndicator } from "./modals/conflict-resolver";
|
||||
|
||||
// Panel components
|
||||
export { HelpDetail } from "./panels/help-detail";
|
||||
export { TodoPanel } from "./panels/todo-panel";
|
||||
export type { TodoItem, Plan } from "./panels/todo-panel";
|
||||
export { DiffView, parseDiffOutput, isDiffContent } from "./panels/diff-view";
|
||||
export { MultiAgentPanel } from "./panels/multi-agent-panel";
|
||||
|
||||
7
src/tui-solid/components/inputs/index.ts
Normal file
7
src/tui-solid/components/inputs/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Input Components
|
||||
*/
|
||||
|
||||
export * from "./input-area";
|
||||
export * from "./file-picker";
|
||||
export * from "./mcp-add-form";
|
||||
8
src/tui-solid/components/layout/index.ts
Normal file
8
src/tui-solid/components/layout/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Layout Components
|
||||
*/
|
||||
|
||||
export * from "./header";
|
||||
export * from "./status-bar";
|
||||
export * from "./logo";
|
||||
export * from "./streaming-message";
|
||||
@@ -4,7 +4,7 @@ import { useTheme } from "@tui-solid/context/theme";
|
||||
import { useAppStore } from "@tui-solid/context/app";
|
||||
import type { LogEntry } from "@/types/tui";
|
||||
import { Spinner } from "@tui-solid/ui/spinner";
|
||||
import { addDebugLog } from "@tui-solid/components/debug-log-panel";
|
||||
import { addDebugLog } from "@tui-solid/components/logs/debug-log-panel";
|
||||
|
||||
interface StreamingMessageProps {
|
||||
entry: LogEntry;
|
||||
@@ -1,236 +0,0 @@
|
||||
import { Show, Switch, Match, For } from "solid-js";
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import { useTheme } from "@tui-solid/context/theme";
|
||||
import type { LogEntry, ToolStatus } from "@/types/tui";
|
||||
import {
|
||||
TOOL_STATUS_ICONS,
|
||||
TOOL_STATUS_COLORS,
|
||||
} from "@constants/tui-components";
|
||||
import { DiffView } from "@tui-solid/components/diff-view";
|
||||
import { StreamingMessage } from "@tui-solid/components/streaming-message";
|
||||
import { parseDiffOutput, isDiffContent } from "@utils/diff/index";
|
||||
|
||||
interface LogEntryDisplayProps {
|
||||
entry: LogEntry;
|
||||
}
|
||||
|
||||
function UserEntry(props: { entry: LogEntry }) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<box flexDirection="column" marginBottom={1}>
|
||||
<text fg={theme.colors.roleUser} attributes={TextAttributes.BOLD}>
|
||||
You
|
||||
</text>
|
||||
<box marginLeft={2}>
|
||||
<text>{props.entry.content}</text>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function AssistantEntry(props: { entry: LogEntry }) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<box flexDirection="column" marginBottom={1}>
|
||||
<text fg={theme.colors.roleAssistant} attributes={TextAttributes.BOLD}>
|
||||
CodeTyper
|
||||
</text>
|
||||
<box marginLeft={2}>
|
||||
<text wrapMode="word">{props.entry.content}</text>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorEntry(props: { entry: LogEntry }) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<box marginBottom={1}>
|
||||
<text fg={theme.colors.error}>✗ Error: {props.entry.content}</text>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function SystemEntry(props: { entry: LogEntry }) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<box marginBottom={1}>
|
||||
<text fg={theme.colors.textDim}>⚙ {props.entry.content}</text>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function ThinkingEntry(props: { entry: LogEntry }) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<box marginBottom={1}>
|
||||
<text fg={theme.colors.modeThinking}>● {props.entry.content}</text>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolEntry(props: { entry: LogEntry }) {
|
||||
const theme = useTheme();
|
||||
const toolStatus = (): ToolStatus =>
|
||||
props.entry.metadata?.toolStatus ?? "pending";
|
||||
const statusIcon = () => TOOL_STATUS_ICONS[toolStatus()];
|
||||
const statusColor = (): string => {
|
||||
const colorKey = TOOL_STATUS_COLORS[toolStatus()];
|
||||
const color = theme.colors[colorKey as keyof typeof theme.colors];
|
||||
if (typeof color === "string") return color;
|
||||
return theme.colors.textDim;
|
||||
};
|
||||
|
||||
const hasDiff = () =>
|
||||
props.entry.metadata?.diffData?.isDiff ||
|
||||
isDiffContent(props.entry.content);
|
||||
|
||||
const isMultiline = () => props.entry.content.includes("\n");
|
||||
const lines = () => props.entry.content.split("\n");
|
||||
|
||||
return (
|
||||
<Switch
|
||||
fallback={
|
||||
<DefaultToolEntry
|
||||
{...props}
|
||||
statusIcon={statusIcon()}
|
||||
statusColor={statusColor()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Match when={hasDiff() && props.entry.metadata?.toolStatus === "success"}>
|
||||
<DiffToolEntry
|
||||
{...props}
|
||||
statusIcon={statusIcon()}
|
||||
statusColor={statusColor()}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={isMultiline()}>
|
||||
<MultilineToolEntry
|
||||
{...props}
|
||||
statusIcon={statusIcon()}
|
||||
statusColor={statusColor()}
|
||||
lines={lines()}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffToolEntry(props: {
|
||||
entry: LogEntry;
|
||||
statusIcon: string;
|
||||
statusColor: string;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const diffData = () => parseDiffOutput(props.entry.content);
|
||||
|
||||
return (
|
||||
<box flexDirection="column" marginBottom={1}>
|
||||
<box flexDirection="row">
|
||||
<text fg={props.statusColor}>{props.statusIcon} </text>
|
||||
<text fg={theme.colors.roleTool}>
|
||||
{props.entry.metadata?.toolName ?? "tool"}
|
||||
</text>
|
||||
<Show when={props.entry.metadata?.toolDescription}>
|
||||
<text fg={theme.colors.textDim}>: </text>
|
||||
<text fg={theme.colors.textDim}>
|
||||
{props.entry.metadata?.toolDescription}
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
<box marginLeft={2}>
|
||||
<DiffView
|
||||
lines={diffData().lines}
|
||||
filePath={diffData().filePath}
|
||||
additions={diffData().additions}
|
||||
deletions={diffData().deletions}
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function MultilineToolEntry(props: {
|
||||
entry: LogEntry;
|
||||
statusIcon: string;
|
||||
statusColor: string;
|
||||
lines: string[];
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const hasDescription = () => Boolean(props.entry.metadata?.toolDescription);
|
||||
|
||||
return (
|
||||
<box flexDirection="column" marginBottom={1}>
|
||||
<box flexDirection="row">
|
||||
<text fg={props.statusColor}>{props.statusIcon} </text>
|
||||
<text fg={theme.colors.roleTool}>
|
||||
{props.entry.metadata?.toolName ?? "tool"}
|
||||
</text>
|
||||
<text fg={theme.colors.textDim}>: </text>
|
||||
<text fg={theme.colors.textDim}>
|
||||
{props.entry.metadata?.toolDescription ?? props.lines[0]}
|
||||
</text>
|
||||
</box>
|
||||
<box flexDirection="column" marginLeft={2}>
|
||||
<For each={hasDescription() ? props.lines : props.lines.slice(1)}>
|
||||
{(line) => <text fg={theme.colors.textDim}>{line}</text>}
|
||||
</For>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function DefaultToolEntry(props: {
|
||||
entry: LogEntry;
|
||||
statusIcon: string;
|
||||
statusColor: string;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<box marginBottom={1} flexDirection="row">
|
||||
<text fg={props.statusColor}>{props.statusIcon} </text>
|
||||
<text fg={theme.colors.roleTool}>
|
||||
{props.entry.metadata?.toolName ?? "tool"}
|
||||
</text>
|
||||
<text fg={theme.colors.textDim}>: </text>
|
||||
<text fg={theme.colors.textDim}>{props.entry.content}</text>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function DefaultEntry(props: { entry: LogEntry }) {
|
||||
return (
|
||||
<box marginBottom={1}>
|
||||
<text>{props.entry.content}</text>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
export function LogEntryDisplay(props: LogEntryDisplayProps) {
|
||||
return (
|
||||
<Switch fallback={<DefaultEntry entry={props.entry} />}>
|
||||
<Match when={props.entry.type === "user"}>
|
||||
<UserEntry entry={props.entry} />
|
||||
</Match>
|
||||
<Match when={props.entry.type === "assistant"}>
|
||||
<AssistantEntry entry={props.entry} />
|
||||
</Match>
|
||||
<Match when={props.entry.type === "assistant_streaming"}>
|
||||
<StreamingMessage entry={props.entry} />
|
||||
</Match>
|
||||
<Match when={props.entry.type === "tool"}>
|
||||
<ToolEntry entry={props.entry} />
|
||||
</Match>
|
||||
<Match when={props.entry.type === "error"}>
|
||||
<ErrorEntry entry={props.entry} />
|
||||
</Match>
|
||||
<Match when={props.entry.type === "system"}>
|
||||
<SystemEntry entry={props.entry} />
|
||||
</Match>
|
||||
<Match when={props.entry.type === "thinking"}>
|
||||
<ThinkingEntry entry={props.entry} />
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
import { createMemo, createSignal, For, Show, onMount, onCleanup } from "solid-js";
|
||||
import { useKeyboard } from "@opentui/solid";
|
||||
import type { ScrollBoxRenderable } from "@opentui/core";
|
||||
import { useTheme } from "@tui-solid/context/theme";
|
||||
import { useAppStore } from "@tui-solid/context/app";
|
||||
import { LogEntryDisplay } from "@tui-solid/components/log-entry";
|
||||
import { ASCII_LOGO, ASCII_LOGO_GRADIENT, HOME_VARS } from "@constants/home";
|
||||
|
||||
const SCROLL_LINES = 3;
|
||||
const MOUSE_ENABLE = "\x1b[?1000h\x1b[?1006h";
|
||||
const MOUSE_DISABLE = "\x1b[?1000l\x1b[?1006l";
|
||||
const SGR_MOUSE_PATTERN = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/;
|
||||
|
||||
const parseMouseScroll = (data: string): "up" | "down" | null => {
|
||||
const match = data.match(SGR_MOUSE_PATTERN);
|
||||
if (!match) return null;
|
||||
|
||||
const button = parseInt(match[1], 10);
|
||||
if (button === 64) return "up";
|
||||
if (button === 65) return "down";
|
||||
return null;
|
||||
};
|
||||
|
||||
export function LogPanel() {
|
||||
const theme = useTheme();
|
||||
const app = useAppStore();
|
||||
let scrollboxRef: ScrollBoxRenderable | undefined;
|
||||
const [stickyEnabled, setStickyEnabled] = createSignal(true);
|
||||
|
||||
const logs = createMemo(() => {
|
||||
return app.logs().filter((entry) => {
|
||||
if (entry.type !== "tool") return true;
|
||||
if (entry.metadata?.quiet) return false;
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
const hasContent = createMemo(() => logs().length > 0);
|
||||
|
||||
const canScroll = createMemo(() => {
|
||||
const mode = app.mode();
|
||||
return (
|
||||
mode === "idle" ||
|
||||
mode === "thinking" ||
|
||||
mode === "tool_execution" ||
|
||||
mode === "editing"
|
||||
);
|
||||
});
|
||||
|
||||
const scrollUp = (): void => {
|
||||
if (!scrollboxRef) return;
|
||||
setStickyEnabled(false);
|
||||
scrollboxRef.scrollBy(-SCROLL_LINES);
|
||||
};
|
||||
|
||||
const scrollDown = (): void => {
|
||||
if (!scrollboxRef) return;
|
||||
scrollboxRef.scrollBy(SCROLL_LINES);
|
||||
|
||||
const isAtBottom =
|
||||
scrollboxRef.scrollTop >=
|
||||
scrollboxRef.content.height - scrollboxRef.viewport.height - 1;
|
||||
if (isAtBottom) {
|
||||
setStickyEnabled(true);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToBottom = (): void => {
|
||||
if (!scrollboxRef) return;
|
||||
scrollboxRef.scrollTo(Infinity);
|
||||
setStickyEnabled(true);
|
||||
};
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (!canScroll()) return;
|
||||
|
||||
if (evt.shift && evt.name === "up") {
|
||||
scrollUp();
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.shift && evt.name === "down") {
|
||||
scrollDown();
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.shift && evt.name === "home") {
|
||||
if (scrollboxRef) {
|
||||
setStickyEnabled(false);
|
||||
scrollboxRef.scrollTo(0);
|
||||
}
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.shift && evt.name === "end") {
|
||||
scrollToBottom();
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
process.stdout.write(MOUSE_ENABLE);
|
||||
|
||||
const handleData = (data: Buffer): void => {
|
||||
if (!canScroll()) return;
|
||||
|
||||
const str = data.toString();
|
||||
const direction = parseMouseScroll(str);
|
||||
|
||||
if (direction === "up") {
|
||||
scrollUp();
|
||||
} else if (direction === "down") {
|
||||
scrollDown();
|
||||
}
|
||||
};
|
||||
|
||||
process.stdin.on("data", handleData);
|
||||
|
||||
onCleanup(() => {
|
||||
process.stdout.write(MOUSE_DISABLE);
|
||||
process.stdin.off("data", handleData);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<box
|
||||
flexDirection="column"
|
||||
flexGrow={1}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
borderColor={theme.colors.border}
|
||||
border={["top", "bottom", "left", "right"]}
|
||||
>
|
||||
<Show
|
||||
when={hasContent()}
|
||||
fallback={
|
||||
<box
|
||||
flexGrow={1}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
flexDirection="column"
|
||||
>
|
||||
<For each={ASCII_LOGO}>
|
||||
{(line, index) => (
|
||||
<text fg={ASCII_LOGO_GRADIENT[index()] ?? theme.colors.primary}>
|
||||
{line}
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
<box marginTop={2}>
|
||||
<text fg={theme.colors.textDim}>{HOME_VARS.subTitle}</text>
|
||||
</box>
|
||||
</box>
|
||||
}
|
||||
>
|
||||
<scrollbox
|
||||
ref={scrollboxRef}
|
||||
stickyScroll={stickyEnabled()}
|
||||
stickyStart="bottom"
|
||||
flexGrow={1}
|
||||
>
|
||||
<box flexDirection="column">
|
||||
<For each={logs()}>
|
||||
{(entry) => <LogEntryDisplay entry={entry} />}
|
||||
</For>
|
||||
</box>
|
||||
</scrollbox>
|
||||
</Show>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
8
src/tui-solid/components/menu/index.ts
Normal file
8
src/tui-solid/components/menu/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Menu Components
|
||||
*/
|
||||
|
||||
export * from "./select-menu";
|
||||
export * from "./command-menu";
|
||||
export * from "./help-menu";
|
||||
export * from "./brain-menu";
|
||||
188
src/tui-solid/components/modals/conflict-resolver.tsx
Normal file
188
src/tui-solid/components/modals/conflict-resolver.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Conflict Resolver
|
||||
*
|
||||
* UI component for displaying and resolving file conflicts between agents.
|
||||
*/
|
||||
|
||||
import { For, Show, createSignal, createMemo, onMount, onCleanup } from "solid-js";
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import { useTheme } from "@tui-solid/context/theme";
|
||||
import { multiAgentStore } from "@stores/core/multi-agent-store";
|
||||
import type { FileConflict, ConflictStrategy } from "@/types/multi-agent";
|
||||
import { CONFLICT_STRATEGY_DESCRIPTIONS } from "@constants/multi-agent";
|
||||
|
||||
interface ConflictResolverProps {
|
||||
visible?: boolean;
|
||||
onResolve?: (filePath: string, strategy: ConflictStrategy) => void;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
const STRATEGY_OPTIONS: Array<{ value: ConflictStrategy; label: string }> = [
|
||||
{ value: "serialize", label: "Wait" },
|
||||
{ value: "abort-newer", label: "Abort Newer" },
|
||||
{ value: "merge-results", label: "Merge" },
|
||||
{ value: "isolated", label: "Isolate" },
|
||||
];
|
||||
|
||||
export function ConflictResolver(props: ConflictResolverProps) {
|
||||
const theme = useTheme();
|
||||
const visible = () => props.visible ?? true;
|
||||
|
||||
const [conflicts, setConflicts] = createSignal<FileConflict[]>([]);
|
||||
const [selectedConflictIndex, setSelectedConflictIndex] = createSignal(0);
|
||||
const [selectedStrategyIndex] = createSignal(0);
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = multiAgentStore.subscribe((state) => {
|
||||
const unresolvedConflicts = state.conflicts.filter((c) => !c.resolution);
|
||||
setConflicts(unresolvedConflicts);
|
||||
|
||||
// Reset selection if current conflict was resolved
|
||||
if (selectedConflictIndex() >= unresolvedConflicts.length) {
|
||||
setSelectedConflictIndex(Math.max(0, unresolvedConflicts.length - 1));
|
||||
}
|
||||
});
|
||||
|
||||
onCleanup(unsubscribe);
|
||||
});
|
||||
|
||||
const currentConflict = createMemo(() => conflicts()[selectedConflictIndex()]);
|
||||
|
||||
const getAgentNames = (agentIds: string[]): string[] => {
|
||||
const state = multiAgentStore.getState();
|
||||
return agentIds.map((id) => {
|
||||
const instance = state.instances.get(id);
|
||||
return instance?.definition.name ?? id;
|
||||
});
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: number): string => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
};
|
||||
|
||||
const selectedStrategy = createMemo(() => STRATEGY_OPTIONS[selectedStrategyIndex()]);
|
||||
|
||||
return (
|
||||
<Show when={visible() && conflicts().length > 0}>
|
||||
<box
|
||||
flexDirection="column"
|
||||
borderColor={theme.colors.warning}
|
||||
border={["top", "bottom", "left", "right"]}
|
||||
padding={1}
|
||||
backgroundColor={theme.colors.background}
|
||||
>
|
||||
{/* Header */}
|
||||
<box flexDirection="row" justifyContent="space-between" marginBottom={1}>
|
||||
<text fg={theme.colors.warning} attributes={TextAttributes.BOLD}>
|
||||
⚠ File Conflict Detected
|
||||
</text>
|
||||
<text fg={theme.colors.textDim}>
|
||||
{selectedConflictIndex() + 1}/{conflicts().length}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
{/* Conflict Details */}
|
||||
<Show when={currentConflict()}>
|
||||
<box flexDirection="column" marginBottom={1}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.colors.textDim}>File:</text>
|
||||
<text fg={theme.colors.text} attributes={TextAttributes.BOLD}>
|
||||
{currentConflict()!.filePath}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.colors.textDim}>Agents:</text>
|
||||
<text fg={theme.colors.text}>
|
||||
{getAgentNames(currentConflict()!.conflictingAgentIds).join(" vs ")}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.colors.textDim}>Detected:</text>
|
||||
<text fg={theme.colors.text}>
|
||||
{formatTime(currentConflict()!.detectedAt)}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
|
||||
{/* Resolution Options */}
|
||||
<box flexDirection="column" marginBottom={1}>
|
||||
<text fg={theme.colors.primary} marginBottom={1}>
|
||||
Resolution Strategy:
|
||||
</text>
|
||||
|
||||
<For each={STRATEGY_OPTIONS}>
|
||||
{(option, index) => (
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
backgroundColor={index() === selectedStrategyIndex() ? theme.colors.bgHighlight : undefined}
|
||||
paddingLeft={1}
|
||||
>
|
||||
<text fg={index() === selectedStrategyIndex() ? theme.colors.primary : theme.colors.textDim}>
|
||||
{index() === selectedStrategyIndex() ? "▸" : " "}
|
||||
</text>
|
||||
<text
|
||||
fg={index() === selectedStrategyIndex() ? theme.colors.text : theme.colors.textDim}
|
||||
attributes={index() === selectedStrategyIndex() ? TextAttributes.BOLD : TextAttributes.NONE}
|
||||
>
|
||||
{option.label}
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
|
||||
{/* Strategy Description */}
|
||||
<box
|
||||
flexDirection="column"
|
||||
marginBottom={1}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
backgroundColor={theme.colors.backgroundPanel}
|
||||
>
|
||||
<text fg={theme.colors.textDim} wrapMode="word">
|
||||
{CONFLICT_STRATEGY_DESCRIPTIONS[selectedStrategy().value]}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
{/* Actions */}
|
||||
<box flexDirection="row" gap={2} justifyContent="flex-end">
|
||||
<text fg={theme.colors.textDim}>
|
||||
[↑/↓] Select [Enter] Resolve [Esc] Dismiss
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact conflict indicator for status bar
|
||||
*/
|
||||
export function ConflictIndicator() {
|
||||
const theme = useTheme();
|
||||
const [conflictCount, setConflictCount] = createSignal(0);
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = multiAgentStore.subscribe((state) => {
|
||||
const unresolvedCount = state.conflicts.filter((c) => !c.resolution).length;
|
||||
setConflictCount(unresolvedCount);
|
||||
});
|
||||
|
||||
onCleanup(unsubscribe);
|
||||
});
|
||||
|
||||
return (
|
||||
<Show when={conflictCount() > 0}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.colors.warning}>
|
||||
⚠ {conflictCount()} conflict{conflictCount() > 1 ? "s" : ""}
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
8
src/tui-solid/components/modals/index.ts
Normal file
8
src/tui-solid/components/modals/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Modal Components
|
||||
*/
|
||||
|
||||
export * from "./centered-modal";
|
||||
export * from "./permission-modal";
|
||||
export * from "./learning-modal";
|
||||
export * from "./conflict-resolver";
|
||||
8
src/tui-solid/components/panels/index.ts
Normal file
8
src/tui-solid/components/panels/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Panel Components
|
||||
*/
|
||||
|
||||
export * from "./todo-panel";
|
||||
export * from "./diff-view";
|
||||
export * from "./multi-agent-panel";
|
||||
export * from "./help-detail";
|
||||
202
src/tui-solid/components/panels/multi-agent-panel.tsx
Normal file
202
src/tui-solid/components/panels/multi-agent-panel.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* Multi-Agent Panel
|
||||
*
|
||||
* Displays active agents, their status, and execution progress.
|
||||
*/
|
||||
|
||||
import { For, Show, createMemo, createSignal, onMount, onCleanup } from "solid-js";
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import { useTheme } from "@tui-solid/context/theme";
|
||||
import { multiAgentStore } from "@stores/core/multi-agent-store";
|
||||
import type { AgentInstance, AgentInstanceStatus } from "@/types/multi-agent";
|
||||
|
||||
interface MultiAgentPanelProps {
|
||||
visible?: boolean;
|
||||
onSelectAgent?: (agentId: string) => void;
|
||||
}
|
||||
|
||||
const STATUS_ICONS: Record<AgentInstanceStatus, string> = {
|
||||
pending: "◯",
|
||||
running: "●",
|
||||
waiting_conflict: "⏸",
|
||||
completed: "✓",
|
||||
error: "✗",
|
||||
cancelled: "⊘",
|
||||
};
|
||||
|
||||
export function MultiAgentPanel(props: MultiAgentPanelProps) {
|
||||
const theme = useTheme();
|
||||
const visible = () => props.visible ?? true;
|
||||
|
||||
const [instances, setInstances] = createSignal<AgentInstance[]>([]);
|
||||
const [selectedIndex] = createSignal(0);
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = multiAgentStore.subscribe((state) => {
|
||||
setInstances(Array.from(state.instances.values()));
|
||||
});
|
||||
|
||||
onCleanup(unsubscribe);
|
||||
});
|
||||
|
||||
const stats = createMemo(() => {
|
||||
const all = instances();
|
||||
return {
|
||||
running: all.filter((i) => i.status === "running").length,
|
||||
waiting: all.filter((i) => i.status === "waiting_conflict").length,
|
||||
completed: all.filter((i) => i.status === "completed").length,
|
||||
failed: all.filter((i) => i.status === "error" || i.status === "cancelled").length,
|
||||
total: all.length,
|
||||
};
|
||||
});
|
||||
|
||||
const getStatusColor = (status: AgentInstanceStatus): string => {
|
||||
const colorMap: Record<AgentInstanceStatus, keyof typeof theme.colors> = {
|
||||
pending: "textDim",
|
||||
running: "info",
|
||||
waiting_conflict: "warning",
|
||||
completed: "success",
|
||||
error: "error",
|
||||
cancelled: "textDim",
|
||||
};
|
||||
return theme.colors[colorMap[status]] as string;
|
||||
};
|
||||
|
||||
const getDuration = (instance: AgentInstance): string => {
|
||||
const end = instance.completedAt ?? Date.now();
|
||||
const duration = end - instance.startedAt;
|
||||
const seconds = Math.floor(duration / 1000);
|
||||
|
||||
if (seconds < 60) {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
};
|
||||
|
||||
const truncateText = (text: string, maxLen: number): string => {
|
||||
if (text.length <= maxLen) return text;
|
||||
return text.substring(0, maxLen - 3) + "...";
|
||||
};
|
||||
|
||||
return (
|
||||
<Show when={visible() && instances().length > 0}>
|
||||
<box
|
||||
flexDirection="column"
|
||||
width={35}
|
||||
borderColor={theme.colors.border}
|
||||
border={["top", "bottom", "left", "right"]}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
>
|
||||
{/* Header */}
|
||||
<box
|
||||
marginBottom={1}
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<text fg={theme.colors.primary} attributes={TextAttributes.BOLD}>
|
||||
Agents
|
||||
</text>
|
||||
<text fg={theme.colors.textDim}>
|
||||
{stats().running}/{stats().total}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
{/* Status Summary */}
|
||||
<box flexDirection="row" gap={1} marginBottom={1}>
|
||||
<Show when={stats().running > 0}>
|
||||
<text fg={theme.colors.info}>
|
||||
● {stats().running}
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={stats().waiting > 0}>
|
||||
<text fg={theme.colors.warning}>
|
||||
⏸ {stats().waiting}
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={stats().completed > 0}>
|
||||
<text fg={theme.colors.success}>
|
||||
✓ {stats().completed}
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={stats().failed > 0}>
|
||||
<text fg={theme.colors.error}>
|
||||
✗ {stats().failed}
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
|
||||
{/* Agent List */}
|
||||
<scrollbox stickyScroll={false} flexGrow={1}>
|
||||
<box flexDirection="column">
|
||||
<For each={instances()}>
|
||||
{(instance, index) => (
|
||||
<box
|
||||
flexDirection="column"
|
||||
marginBottom={1}
|
||||
backgroundColor={index() === selectedIndex() ? theme.colors.bgHighlight : undefined}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={getStatusColor(instance.status)}>
|
||||
{STATUS_ICONS[instance.status]}
|
||||
</text>
|
||||
<text
|
||||
fg={theme.colors.text}
|
||||
attributes={TextAttributes.BOLD}
|
||||
flexGrow={1}
|
||||
>
|
||||
{instance.definition.name}
|
||||
</text>
|
||||
<text fg={theme.colors.textDim}>
|
||||
{getDuration(instance)}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
<box flexDirection="row" marginLeft={2}>
|
||||
<text fg={theme.colors.textDim}>
|
||||
{truncateText(instance.config.task, 25)}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
<Show when={instance.status === "error" && instance.error}>
|
||||
<box flexDirection="row" marginLeft={2}>
|
||||
<text fg={theme.colors.error}>
|
||||
{truncateText(instance.error ?? "", 30)}
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
|
||||
<Show when={instance.modifiedFiles.length > 0}>
|
||||
<box flexDirection="row" marginLeft={2}>
|
||||
<text fg={theme.colors.textDim}>
|
||||
{instance.modifiedFiles.length} file(s) modified
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</scrollbox>
|
||||
|
||||
{/* Footer with conflicts */}
|
||||
<Show when={stats().waiting > 0}>
|
||||
<box
|
||||
marginTop={1}
|
||||
paddingTop={1}
|
||||
borderColor={theme.colors.border}
|
||||
border={["top"]}
|
||||
>
|
||||
<text fg={theme.colors.warning}>
|
||||
⚠ {stats().waiting} agent(s) waiting on conflicts
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
10
src/tui-solid/components/submenu/index.ts
Normal file
10
src/tui-solid/components/submenu/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Submenu Components
|
||||
*/
|
||||
|
||||
export * from "./agent-select";
|
||||
export * from "./model-select";
|
||||
export * from "./provider-select";
|
||||
export * from "./theme-select";
|
||||
export * from "./mode-select";
|
||||
export * from "./mcp-select";
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Match, Switch } from "solid-js";
|
||||
import { useTheme } from "@tui-solid/context/theme";
|
||||
import { useAppStore } from "@tui-solid/context/app";
|
||||
import { Logo } from "@tui-solid/components/logo";
|
||||
import { InputArea } from "@tui-solid/components/input-area";
|
||||
import { CommandMenu } from "@tui-solid/components/command-menu";
|
||||
import { ModelSelect } from "@tui-solid/components/model-select";
|
||||
import { ThemeSelect } from "@tui-solid/components/theme-select";
|
||||
import { FilePicker } from "@tui-solid/components/file-picker";
|
||||
import { CenteredModal } from "@tui-solid/components/centered-modal";
|
||||
import { Logo } from "@tui-solid/components/layout/logo";
|
||||
import { InputArea } from "@tui-solid/components/inputs/input-area";
|
||||
import { CommandMenu } from "@tui-solid/components/menu/command-menu";
|
||||
import { ModelSelect } from "@tui-solid/components/submenu/model-select";
|
||||
import { ThemeSelect } from "@tui-solid/components/submenu/theme-select";
|
||||
import { FilePicker } from "@tui-solid/components/inputs/file-picker";
|
||||
import { CenteredModal } from "@tui-solid/components/modals/centered-modal";
|
||||
import { HOME_VARS } from "@constants/home";
|
||||
|
||||
interface HomeProps {
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { Show, Switch, Match, createSignal, createMemo, onMount } from "solid-js";
|
||||
import { useTheme } from "@tui-solid/context/theme";
|
||||
import { useAppStore } from "@tui-solid/context/app";
|
||||
import { Header } from "@tui-solid/components/header";
|
||||
import { LogPanel } from "@tui-solid/components/log-panel";
|
||||
import { InputArea } from "@tui-solid/components/input-area";
|
||||
import { StatusBar } from "@tui-solid/components/status-bar";
|
||||
import { CommandMenu } from "@tui-solid/components/command-menu";
|
||||
import { ModelSelect } from "@tui-solid/components/model-select";
|
||||
import { ThemeSelect } from "@tui-solid/components/theme-select";
|
||||
import { AgentSelect } from "@tui-solid/components/agent-select";
|
||||
import { MCPSelect } from "@tui-solid/components/mcp-select";
|
||||
import { MCPAddForm } from "@tui-solid/components/mcp-add-form";
|
||||
import { ModeSelect } from "@tui-solid/components/mode-select";
|
||||
import { ProviderSelect } from "@tui-solid/components/provider-select";
|
||||
import { FilePicker } from "@tui-solid/components/file-picker";
|
||||
import { PermissionModal } from "@tui-solid/components/permission-modal";
|
||||
import { LearningModal } from "@tui-solid/components/learning-modal";
|
||||
import { HelpMenu } from "@tui-solid/components/help-menu";
|
||||
import { HelpDetail } from "@tui-solid/components/help-detail";
|
||||
import { TodoPanel } from "@tui-solid/components/todo-panel";
|
||||
import { CenteredModal } from "@tui-solid/components/centered-modal";
|
||||
import { DebugLogPanel } from "@tui-solid/components/debug-log-panel";
|
||||
import { BrainMenu } from "@tui-solid/components/brain-menu";
|
||||
import { Header } from "@tui-solid/components/layout/header";
|
||||
import { LogPanel } from "@tui-solid/components/logs/log-panel";
|
||||
import { InputArea } from "@tui-solid/components/inputs/input-area";
|
||||
import { StatusBar } from "@tui-solid/components/layout/status-bar";
|
||||
import { CommandMenu } from "@tui-solid/components/menu/command-menu";
|
||||
import { ModelSelect } from "@tui-solid/components/submenu/model-select";
|
||||
import { ThemeSelect } from "@tui-solid/components/submenu/theme-select";
|
||||
import { AgentSelect } from "@tui-solid/components/submenu/agent-select";
|
||||
import { MCPSelect } from "@tui-solid/components/submenu/mcp-select";
|
||||
import { MCPAddForm } from "@tui-solid/components/inputs/mcp-add-form";
|
||||
import { ModeSelect } from "@tui-solid/components/submenu/mode-select";
|
||||
import { ProviderSelect } from "@tui-solid/components/submenu/provider-select";
|
||||
import { FilePicker } from "@tui-solid/components/inputs/file-picker";
|
||||
import { PermissionModal } from "@tui-solid/components/modals/permission-modal";
|
||||
import { LearningModal } from "@tui-solid/components/modals/learning-modal";
|
||||
import { HelpMenu } from "@tui-solid/components/menu/help-menu";
|
||||
import { HelpDetail } from "@tui-solid/components/panels/help-detail";
|
||||
import { TodoPanel } from "@tui-solid/components/panels/todo-panel";
|
||||
import { CenteredModal } from "@tui-solid/components/modals/centered-modal";
|
||||
import { DebugLogPanel } from "@tui-solid/components/logs/debug-log-panel";
|
||||
import { BrainMenu } from "@tui-solid/components/menu/brain-menu";
|
||||
import { BRAIN_DISABLED } from "@constants/brain";
|
||||
import { initializeMCP, getServerInstances } from "@services/mcp";
|
||||
import type { PermissionScope, LearningScope, InteractionMode, MCPServerDisplay } from "@/types/tui";
|
||||
|
||||
Reference in New Issue
Block a user