Add BRAIN_DISABLED flag and fix Ollama tool call formatting

Features:
  - Add BRAIN_DISABLED feature flag to hide all Brain functionality
  - When enabled, hides Brain banner, status indicator, menu, and commands
  - Flag location: src/constants/brain.ts

  Fixes:
  - Fix Ollama 400 error by properly formatting tool_calls in messages
  - Update OllamaMessage type to include tool_calls field
  - Fix Brain menu keyboard not working (add missing modes to isMenuOpen)

  UI Changes:
  - Remove "^Tab toggle mode" hint from status bar
  - Remove "ctrl+t to hide todos" hint from status bar

  Files modified:
  - src/constants/brain.ts (add BRAIN_DISABLED flag)
  - src/types/ollama.ts (add tool_calls to OllamaMessage)
  - src/providers/ollama/chat.ts (format tool_calls in messages)
  - src/tui-solid/components/header.tsx (hide Brain UI when disabled)
  - src/tui-solid/components/status-bar.tsx (remove hints)
  - src/tui-solid/components/command-menu.tsx (filter brain command)
  - src/tui-solid/components/input-area.tsx (fix isMenuOpen modes)
  - src/tui-solid/routes/session.tsx (skip brain menu when disabled)
  - src/services/brain.ts (early return when disabled)
  - src/services/chat-tui/initialize.ts (skip brain init when disabled)
This commit is contained in:
2026-02-02 13:25:38 -05:00
parent 2eadda584a
commit c839fc4d68
114 changed files with 17243 additions and 273 deletions

View File

@@ -62,6 +62,9 @@ interface AppProps extends TuiInput {
scope?: LearningScope,
editedContent?: string,
) => void;
onBrainSetJwtToken?: (jwtToken: string) => Promise<void>;
onBrainSetApiKey?: (apiKey: string) => Promise<void>;
onBrainLogout?: () => Promise<void>;
plan?: {
id: string;
title: string;
@@ -139,9 +142,14 @@ function AppContent(props: AppProps) {
app.setCascadeEnabled(props.cascadeEnabled);
}
// Navigate to session if resuming
if (props.sessionId) {
route.goToSession(props.sessionId);
// Always navigate to session view (skip home page)
// Use existing sessionId or create a new one
if (!route.isSession()) {
const sessionId = props.sessionId ?? `session-${Date.now()}`;
batch(() => {
app.setSessionInfo(sessionId, app.provider(), app.model());
route.goToSession(sessionId);
});
}
if (props.availableModels && props.availableModels.length > 0) {
@@ -375,6 +383,9 @@ function AppContent(props: AppProps) {
onCascadeToggle={handleCascadeToggle}
onPermissionResponse={handlePermissionResponse}
onLearningResponse={handleLearningResponse}
onBrainSetJwtToken={props.onBrainSetJwtToken}
onBrainSetApiKey={props.onBrainSetApiKey}
onBrainLogout={props.onBrainLogout}
plan={props.plan}
agents={props.agents}
currentAgent={props.currentAgent}
@@ -429,6 +440,9 @@ export interface TuiRenderOptions extends TuiInput {
scope?: LearningScope,
editedContent?: string,
) => void;
onBrainSetJwtToken?: (jwtToken: string) => Promise<void>;
onBrainSetApiKey?: (apiKey: string) => Promise<void>;
onBrainLogout?: () => Promise<void>;
plan?: {
id: string;
title: string;

View File

@@ -0,0 +1,411 @@
import { createSignal, Show, For } from "solid-js";
import { useKeyboard } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { useTheme } from "@tui-solid/context/theme";
import { useAppStore } from "@tui-solid/context/app";
import { BRAIN_BANNER } from "@constants/brain";
interface BrainMenuProps {
onSetJwtToken: (jwtToken: string) => Promise<void>;
onSetApiKey: (apiKey: string) => Promise<void>;
onLogout: () => Promise<void>;
onClose: () => void;
isActive?: boolean;
}
type MenuView = "main" | "login_url" | "jwt_input" | "apikey";
interface MenuItem {
id: string;
label: string;
description: string;
action: () => void;
disabled?: boolean;
}
export function BrainMenu(props: BrainMenuProps) {
const theme = useTheme();
const app = useAppStore();
const isActive = () => props.isActive ?? true;
const [view, setView] = createSignal<MenuView>("main");
const [selectedIndex, setSelectedIndex] = createSignal(0);
const [jwtToken, setJwtToken] = createSignal("");
const [apiKey, setApiKey] = createSignal("");
const [error, setError] = createSignal<string | null>(null);
const [loading, setLoading] = createSignal(false);
const isConnected = () => app.brain().status === "connected";
const menuItems = (): MenuItem[] => {
const items: MenuItem[] = [];
if (!isConnected()) {
items.push({
id: "login",
label: "Login with Email",
description: "Get JWT token from the web portal",
action: () => {
setView("login_url");
setSelectedIndex(0);
},
});
items.push({
id: "apikey",
label: "Use API Key",
description: "Enter your API key directly",
action: () => {
setView("apikey");
setSelectedIndex(0);
},
});
} else {
items.push({
id: "logout",
label: "Logout",
description: "Disconnect from CodeTyper Brain",
action: async () => {
setLoading(true);
try {
await props.onLogout();
} catch (err) {
setError(err instanceof Error ? err.message : "Logout failed");
} finally {
setLoading(false);
}
},
});
}
items.push({
id: "close",
label: "Close",
description: "Return to session",
action: () => props.onClose(),
});
return items;
};
const handleJwtSubmit = async (): Promise<void> => {
if (!jwtToken()) {
setError("JWT token is required");
return;
}
setLoading(true);
setError(null);
try {
await props.onSetJwtToken(jwtToken());
setView("main");
setJwtToken("");
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to set JWT token");
} finally {
setLoading(false);
}
};
const handleApiKey = async (): Promise<void> => {
if (!apiKey()) {
setError("API key is required");
return;
}
setLoading(true);
setError(null);
try {
await props.onSetApiKey(apiKey());
setView("main");
setApiKey("");
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to set API key");
} finally {
setLoading(false);
}
};
useKeyboard((evt) => {
if (!isActive()) return;
if (evt.name === "escape") {
if (view() !== "main") {
setView("main");
setError(null);
setSelectedIndex(0);
} else {
props.onClose();
}
evt.preventDefault();
evt.stopPropagation();
return;
}
// Main menu navigation
if (view() === "main") {
if (evt.name === "up") {
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : menuItems().length - 1));
evt.preventDefault();
return;
}
if (evt.name === "down") {
setSelectedIndex((prev) => (prev < menuItems().length - 1 ? prev + 1 : 0));
evt.preventDefault();
return;
}
if (evt.name === "return") {
const item = menuItems()[selectedIndex()];
if (item && !item.disabled) {
item.action();
}
evt.preventDefault();
return;
}
}
// Login URL view - press Enter to go to JWT input
if (view() === "login_url") {
if (evt.name === "return") {
setView("jwt_input");
evt.preventDefault();
return;
}
}
// JWT token input handling
if (view() === "jwt_input") {
if (evt.name === "return") {
handleJwtSubmit();
evt.preventDefault();
return;
}
if (evt.name === "backspace") {
setJwtToken((prev) => prev.slice(0, -1));
evt.preventDefault();
return;
}
if (evt.name.length === 1 && !evt.ctrl && !evt.meta) {
setJwtToken((prev) => prev + evt.name);
evt.preventDefault();
return;
}
}
// API key form handling
if (view() === "apikey") {
if (evt.name === "return") {
handleApiKey();
evt.preventDefault();
return;
}
if (evt.name === "backspace") {
setApiKey((prev) => prev.slice(0, -1));
evt.preventDefault();
return;
}
if (evt.name.length === 1 && !evt.ctrl && !evt.meta) {
setApiKey((prev) => prev + evt.name);
evt.preventDefault();
return;
}
}
});
const getStatusColor = (): string => {
const status = app.brain().status;
const colorMap: Record<string, string> = {
connected: theme.colors.success,
connecting: theme.colors.warning,
disconnected: theme.colors.textDim,
error: theme.colors.error,
};
return colorMap[status] ?? theme.colors.textDim;
};
const getStatusText = (): string => {
const status = app.brain().status;
const textMap: Record<string, string> = {
connected: "Connected",
connecting: "Connecting...",
disconnected: "Not connected",
error: "Connection error",
};
return textMap[status] ?? "Unknown";
};
return (
<box
flexDirection="column"
borderColor={theme.colors.accent}
border={["top", "bottom", "left", "right"]}
backgroundColor={theme.colors.background}
paddingLeft={1}
paddingRight={1}
width={60}
>
{/* Header */}
<box marginBottom={1} flexDirection="row" gap={1}>
<text fg="#ff69b4" attributes={TextAttributes.BOLD}>
{BRAIN_BANNER.EMOJI_CONNECTED}
</text>
<text fg={theme.colors.accent} attributes={TextAttributes.BOLD}>
CodeTyper Brain
</text>
</box>
{/* Status */}
<box marginBottom={1} flexDirection="row">
<text fg={theme.colors.textDim}>Status: </text>
<text fg={getStatusColor()}>{getStatusText()}</text>
<Show when={isConnected()}>
<text fg={theme.colors.textDim}>
{" "}({app.brain().knowledgeCount}K / {app.brain().memoryCount}M)
</text>
</Show>
</box>
<Show when={isConnected() && app.brain().user}>
<box marginBottom={1} flexDirection="row">
<text fg={theme.colors.textDim}>User: </text>
<text fg={theme.colors.info}>
{app.brain().user?.display_name ?? app.brain().user?.email}
</text>
</box>
</Show>
{/* Error message */}
<Show when={error()}>
<box marginBottom={1}>
<text fg={theme.colors.error}>{error()}</text>
</box>
</Show>
{/* Main menu view */}
<Show when={view() === "main"}>
<box flexDirection="column">
<For each={menuItems()}>
{(item, index) => {
const isSelected = () => index() === selectedIndex();
return (
<box flexDirection="column" marginBottom={1}>
<box flexDirection="row">
<text
fg={isSelected() ? theme.colors.accent : undefined}
attributes={isSelected() ? TextAttributes.BOLD : TextAttributes.NONE}
>
{isSelected() ? "> " : " "}
</text>
<text
fg={isSelected() ? theme.colors.accent : undefined}
attributes={isSelected() ? TextAttributes.BOLD : TextAttributes.NONE}
>
{item.label}
</text>
</box>
<box marginLeft={4}>
<text fg={theme.colors.textDim}>{item.description}</text>
</box>
</box>
);
}}
</For>
</box>
<box marginTop={1} flexDirection="column">
<text fg={theme.colors.info}>{BRAIN_BANNER.CTA}: {BRAIN_BANNER.URL}</text>
<text fg={theme.colors.textDim}>
Arrow keys navigate | Enter select | Esc close
</text>
</box>
</Show>
{/* Login URL view - shows where to login */}
<Show when={view() === "login_url"}>
<box flexDirection="column">
<box marginBottom={1}>
<text fg={theme.colors.text}>1. Go to this page to login:</text>
</box>
<box marginBottom={1}>
<text fg={theme.colors.accent} attributes={TextAttributes.BOLD}>
{BRAIN_BANNER.LOGIN_URL}
</text>
</box>
<box marginBottom={1}>
<text fg={theme.colors.text}>2. After logging in, copy your JWT token</text>
</box>
<box marginBottom={1}>
<text fg={theme.colors.text}>3. Press Enter to input your token</text>
</box>
</box>
<box marginTop={1}>
<text fg={theme.colors.textDim}>
Enter continue | Esc back
</text>
</box>
</Show>
{/* JWT token input view */}
<Show when={view() === "jwt_input"}>
<box flexDirection="column">
<box marginBottom={1} flexDirection="column">
<text fg={theme.colors.accent}>JWT Token:</text>
<box
borderColor={theme.colors.accent}
border={["top", "bottom", "left", "right"]}
paddingLeft={1}
paddingRight={1}
>
<text fg={theme.colors.text}>
{jwtToken() ? "*".repeat(Math.min(jwtToken().length, 40)) : " "}
</text>
</box>
</box>
<Show when={loading()}>
<text fg={theme.colors.warning}>Saving token...</text>
</Show>
</box>
<box marginTop={1}>
<text fg={theme.colors.textDim}>Enter save | Esc back</text>
</box>
</Show>
{/* API key form view */}
<Show when={view() === "apikey"}>
<box flexDirection="column">
<box marginBottom={1} flexDirection="column">
<text fg={theme.colors.accent}>API Key:</text>
<box
borderColor={theme.colors.accent}
border={["top", "bottom", "left", "right"]}
paddingLeft={1}
paddingRight={1}
>
<text fg={theme.colors.text}>
{apiKey() ? "*".repeat(Math.min(apiKey().length, 40)) : " "}
</text>
</box>
</box>
<Show when={loading()}>
<text fg={theme.colors.warning}>Setting API key...</text>
</Show>
</box>
<box marginTop={1}>
<text fg={theme.colors.textDim}>Enter save | Esc back</text>
</box>
</Show>
</box>
);
}

View File

@@ -3,6 +3,7 @@ import { useKeyboard } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { useTheme } from "@tui-solid/context/theme";
import { useAppStore } from "@tui-solid/context/app";
import { BRAIN_DISABLED } from "@constants/brain";
import type { SlashCommand, CommandCategory } from "@/types/tui";
import { SLASH_COMMANDS, COMMAND_CATEGORIES } from "@constants/tui-components";
@@ -22,9 +23,14 @@ const filterCommands = (
commands: readonly SlashCommand[],
filter: string,
): SlashCommand[] => {
if (!filter) return [...commands];
// Filter out brain command when Brain is disabled
let availableCommands = BRAIN_DISABLED
? commands.filter((cmd) => cmd.name !== "brain")
: [...commands];
if (!filter) return availableCommands;
const query = filter.toLowerCase();
return commands.filter(
return availableCommands.filter(
(cmd) =>
cmd.name.toLowerCase().includes(query) ||
cmd.description.toLowerCase().includes(query),

View File

@@ -156,11 +156,11 @@ export function DebugLogPanel() {
paddingRight={1}
borderColor={theme.colors.border}
border={["bottom"]}
flexDirection="row"
>
<text fg={theme.colors.accent} attributes={TextAttributes.BOLD}>
Debug Logs
Debug Logs ({entries().length})
</text>
<text fg={theme.colors.textDim}> ({entries().length})</text>
</box>
<scrollbox

View File

@@ -2,6 +2,11 @@ import { Show, createMemo } from "solid-js";
import { TextAttributes } from "@opentui/core";
import { useTheme } from "@tui-solid/context/theme";
import { useAppStore } from "@tui-solid/context/app";
import { BRAIN_BANNER, BRAIN_DISABLED } from "@constants/brain";
import {
TOKEN_WARNING_THRESHOLD,
TOKEN_CRITICAL_THRESHOLD,
} from "@constants/token";
interface HeaderProps {
showBanner?: boolean;
@@ -25,6 +30,30 @@ const MODE_COLORS = {
"code-review": "success",
} as const;
const BRAIN_STATUS_COLORS = {
connected: "success",
connecting: "warning",
disconnected: "textDim",
error: "error",
} as const;
const TOKEN_STATUS_COLORS = {
normal: "textDim",
warning: "warning",
critical: "error",
compacting: "info",
} as const;
/**
* Format token count for display (e.g., 45.2K)
*/
const formatTokenCount = (tokens: number): string => {
if (tokens >= 1000) {
return `${(tokens / 1000).toFixed(1)}K`;
}
return tokens.toString();
};
export function Header(props: HeaderProps) {
const theme = useTheme();
const app = useAppStore();
@@ -35,57 +64,162 @@ export function Header(props: HeaderProps) {
return theme.colors[colorKey];
});
return (
<box
flexDirection="row"
justifyContent="space-between"
paddingLeft={1}
paddingRight={1}
borderColor={theme.colors.border}
border={["bottom"]}
>
<box flexDirection="row" gap={1}>
<Show when={showBanner()}>
<text fg={theme.colors.primary} attributes={TextAttributes.BOLD}>
CodeTyper
</text>
</Show>
<text fg={theme.colors.textDim}>v{app.version()}</text>
<text fg={theme.colors.textDim}>|</text>
<box flexDirection="row">
<text fg={modeColor()} attributes={TextAttributes.BOLD}>
[{MODE_LABELS[app.interactionMode()]}]
</text>
<Show when={app.currentAgent() !== "default"}>
<text fg={theme.colors.secondary} attributes={TextAttributes.BOLD}>
{" "}
@{app.currentAgent()}
</text>
</Show>
<text fg={theme.colors.textDim}>
{" "}
- {MODE_DESCRIPTIONS[app.interactionMode()]}
</text>
</box>
</box>
const brainColor = createMemo(() => {
const brain = app.brain();
const colorKey = BRAIN_STATUS_COLORS[brain.status];
return theme.colors[colorKey];
});
<box flexDirection="row" gap={2}>
<box flexDirection="row">
<text fg={theme.colors.textDim}>Provider: </text>
<text fg={theme.colors.secondary}>{app.provider()}</text>
</box>
<box flexDirection="row">
<text fg={theme.colors.textDim}>Model: </text>
<text fg={theme.colors.accent}>{app.model() || "auto"}</text>
</box>
<Show when={app.sessionId()}>
<box flexDirection="row">
<text fg={theme.colors.textDim}>Session: </text>
<text fg={theme.colors.info}>
{app.sessionId()?.replace("session-", "").slice(-5)}
const shouldShowBrainBanner = createMemo(() => {
if (BRAIN_DISABLED) return false;
const brain = app.brain();
return brain.showBanner && brain.status === "disconnected";
});
// Context window usage calculation
const contextUsage = createMemo(() => {
const stats = app.sessionStats();
const totalTokens = stats.inputTokens + stats.outputTokens;
const maxTokens = stats.contextMaxTokens;
const usagePercent = maxTokens > 0 ? (totalTokens / maxTokens) * 100 : 0;
let status: "normal" | "warning" | "critical" | "compacting" = "normal";
if (app.isCompacting()) {
status = "compacting";
} else if (usagePercent >= TOKEN_CRITICAL_THRESHOLD * 100) {
status = "critical";
} else if (usagePercent >= TOKEN_WARNING_THRESHOLD * 100) {
status = "warning";
}
return {
current: totalTokens,
max: maxTokens,
percent: usagePercent,
status,
};
});
const tokenColor = createMemo(() => {
const colorKey = TOKEN_STATUS_COLORS[contextUsage().status];
return theme.colors[colorKey];
});
return (
<box flexDirection="column">
{/* Brain Banner - shown when not connected */}
<Show when={shouldShowBrainBanner()}>
<box
flexDirection="row"
justifyContent="space-between"
paddingLeft={1}
paddingRight={1}
backgroundColor="#1a1a2e"
>
<box flexDirection="row" gap={1}>
<text fg="#ff69b4" attributes={TextAttributes.BOLD}>
{BRAIN_BANNER.EMOJI_CONNECTED}
</text>
<text fg="#ffffff" attributes={TextAttributes.BOLD}>
{BRAIN_BANNER.TITLE}
</text>
<text fg={theme.colors.textDim}>-</text>
<text fg={theme.colors.textDim}>{BRAIN_BANNER.CTA}:</text>
<text fg={theme.colors.info} attributes={TextAttributes.UNDERLINE}>
{BRAIN_BANNER.URL}
</text>
</box>
</Show>
<text fg={theme.colors.textDim}>[Ctrl+B dismiss]</text>
</box>
</Show>
{/* Main Header */}
<box
flexDirection="row"
justifyContent="space-between"
paddingLeft={1}
paddingRight={1}
borderColor={theme.colors.border}
border={["bottom"]}
>
<box flexDirection="row" gap={1}>
<Show when={showBanner()}>
<text fg={theme.colors.primary} attributes={TextAttributes.BOLD}>
CodeTyper
</text>
</Show>
<text fg={theme.colors.textDim}>v{app.version()}</text>
<text fg={theme.colors.textDim}>|</text>
<box flexDirection="row">
<text fg={modeColor()} attributes={TextAttributes.BOLD}>
[{MODE_LABELS[app.interactionMode()]}]
</text>
<Show when={app.currentAgent() !== "default"}>
<text fg={theme.colors.secondary} attributes={TextAttributes.BOLD}>
{" "}
@{app.currentAgent()}
</text>
</Show>
<text fg={theme.colors.textDim}>
{" "}
- {MODE_DESCRIPTIONS[app.interactionMode()]}
</text>
</box>
</box>
<box flexDirection="row" gap={2}>
{/* Context Window Usage */}
<Show when={contextUsage().max > 0}>
<box flexDirection="row">
<text fg={tokenColor()}>
{formatTokenCount(contextUsage().current)}
</text>
<text fg={theme.colors.textDim}>/</text>
<text fg={theme.colors.textDim}>
{formatTokenCount(contextUsage().max)}
</text>
<Show when={contextUsage().status === "compacting"}>
<text fg={theme.colors.info}> [compacting]</text>
</Show>
</box>
</Show>
{/* Brain Status Indicator - hidden when BRAIN_DISABLED */}
<Show when={!BRAIN_DISABLED}>
<box flexDirection="row">
<text fg={brainColor()}>
{app.brain().status === "connected"
? BRAIN_BANNER.EMOJI_CONNECTED
: app.brain().status === "connecting"
? "..."
: BRAIN_BANNER.EMOJI_DISCONNECTED}
</text>
<Show when={app.brain().status === "connected"}>
<text fg={theme.colors.textDim}>
{" "}
{app.brain().knowledgeCount}K/{app.brain().memoryCount}M
</text>
</Show>
</box>
</Show>
<box flexDirection="row">
<text fg={theme.colors.textDim}>Provider: </text>
<text fg={theme.colors.secondary}>{app.provider()}</text>
</box>
<box flexDirection="row">
<text fg={theme.colors.textDim}>Model: </text>
<text fg={theme.colors.accent}>{app.model() || "auto"}</text>
</box>
<Show when={app.sessionId()}>
<box flexDirection="row">
<text fg={theme.colors.textDim}>Session: </text>
<text fg={theme.colors.info}>
{app.sessionId()?.replace("session-", "").slice(-5)}
</text>
</box>
</Show>
</box>
</box>
</box>
);

View File

@@ -94,7 +94,11 @@ export function InputArea(props: InputAreaProps) {
mode === "permission_prompt" ||
mode === "learning_prompt" ||
mode === "help_menu" ||
mode === "help_detail"
mode === "help_detail" ||
mode === "brain_menu" ||
mode === "brain_login" ||
mode === "provider_select" ||
mode === "mcp_browse"
);
});
const placeholder = () =>
@@ -108,10 +112,10 @@ export function InputArea(props: InputAreaProps) {
// Handle "/" to open command menu when input is empty
// Handle Enter to submit (backup in case onSubmit doesn't fire)
// Handle Ctrl+Tab to toggle interaction mode
// Handle Ctrl+M to toggle interaction mode (Ctrl+Tab doesn't work in most terminals)
useKeyboard((evt) => {
// Ctrl+Tab works even when locked or menus are open
if (evt.ctrl && evt.name === "tab") {
// Ctrl+M works even when locked or menus are open
if (evt.ctrl && evt.name === "m") {
app.toggleInteractionMode();
evt.preventDefault();
evt.stopPropagation();

View File

@@ -4,6 +4,7 @@ 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";
@@ -141,10 +142,22 @@ export function LogPanel() {
<Show
when={hasContent()}
fallback={
<box flexGrow={1} alignItems="center" justifyContent="center">
<text fg={theme.colors.textDim}>
No messages yet. Type your prompt below.
</text>
<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>
}
>

View File

@@ -155,11 +155,6 @@ export function StatusBar() {
const hints = createMemo(() => {
const result: string[] = [];
// Show mode toggle hint when idle
if (!isProcessing()) {
result.push("^Tab toggle mode");
}
if (isProcessing()) {
result.push(
app.interruptPending()
@@ -168,10 +163,6 @@ export function StatusBar() {
);
}
if (app.todosVisible()) {
result.push(STATUS_HINTS.TOGGLE_TODOS);
}
result.push(formatDuration(elapsed()));
if (totalTokens() > 0) {

View File

@@ -16,6 +16,7 @@ import type {
SuggestionState,
} from "@/types/tui";
import type { ProviderModel } from "@/types/providers";
import type { BrainConnectionStatus, BrainUser } from "@/types/brain";
interface AppStore {
mode: AppMode;
@@ -44,6 +45,13 @@ interface AppStore {
streamingLog: StreamingLogState;
suggestions: SuggestionState;
cascadeEnabled: boolean;
brain: {
status: BrainConnectionStatus;
user: BrainUser | null;
knowledgeCount: number;
memoryCount: number;
showBanner: boolean;
};
}
interface AppContextValue {
@@ -81,6 +89,13 @@ interface AppContextValue {
streamingLogIsActive: Accessor<boolean>;
suggestions: Accessor<SuggestionState>;
cascadeEnabled: Accessor<boolean>;
brain: Accessor<{
status: BrainConnectionStatus;
user: BrainUser | null;
knowledgeCount: number;
memoryCount: number;
showBanner: boolean;
}>;
// Mode actions
setMode: (mode: AppMode) => void;
@@ -138,6 +153,7 @@ interface AppContextValue {
stopThinking: () => void;
addTokens: (input: number, output: number) => void;
resetSessionStats: () => void;
setContextMaxTokens: (maxTokens: number) => void;
// UI state actions
toggleTodos: () => void;
@@ -161,6 +177,13 @@ interface AppContextValue {
hideSuggestions: () => void;
showSuggestions: () => void;
// Brain actions
setBrainStatus: (status: BrainConnectionStatus) => void;
setBrainUser: (user: BrainUser | null) => void;
setBrainCounts: (knowledge: number, memory: number) => void;
setBrainShowBanner: (show: boolean) => void;
dismissBrainBanner: () => void;
// Computed
isInputLocked: () => boolean;
}
@@ -174,6 +197,7 @@ const createInitialSessionStats = (): SessionStats => ({
outputTokens: 0,
thinkingStartTime: null,
lastThinkingDuration: 0,
contextMaxTokens: 128000, // Default, updated when model is selected
});
const createInitialStreamingState = (): StreamingLogState => ({
@@ -225,6 +249,13 @@ export const { provider: AppStoreProvider, use: useAppStore } =
streamingLog: createInitialStreamingState(),
suggestions: createInitialSuggestionState(),
cascadeEnabled: true,
brain: {
status: "disconnected" as BrainConnectionStatus,
user: null,
knowledgeCount: 0,
memoryCount: 0,
showBanner: true,
},
});
// Input insert function (set by InputArea)
@@ -272,6 +303,7 @@ export const { provider: AppStoreProvider, use: useAppStore } =
const streamingLogIsActive = (): boolean => store.streamingLog.isStreaming;
const suggestions = (): SuggestionState => store.suggestions;
const cascadeEnabled = (): boolean => store.cascadeEnabled;
const brain = () => store.brain;
// Mode actions
const setMode = (newMode: AppMode): void => {
@@ -469,6 +501,27 @@ export const { provider: AppStoreProvider, use: useAppStore } =
setStore("cascadeEnabled", !store.cascadeEnabled);
};
// Brain actions
const setBrainStatus = (status: BrainConnectionStatus): void => {
setStore("brain", { ...store.brain, status });
};
const setBrainUser = (user: BrainUser | null): void => {
setStore("brain", { ...store.brain, user });
};
const setBrainCounts = (knowledgeCount: number, memoryCount: number): void => {
setStore("brain", { ...store.brain, knowledgeCount, memoryCount });
};
const setBrainShowBanner = (showBanner: boolean): void => {
setStore("brain", { ...store.brain, showBanner });
};
const dismissBrainBanner = (): void => {
setStore("brain", { ...store.brain, showBanner: false });
};
// Session stats actions
const startThinking = (): void => {
setStore("sessionStats", {
@@ -502,6 +555,13 @@ export const { provider: AppStoreProvider, use: useAppStore } =
setStore("sessionStats", createInitialSessionStats());
};
const setContextMaxTokens = (maxTokens: number): void => {
setStore("sessionStats", {
...store.sessionStats,
contextMaxTokens: maxTokens,
});
};
// UI state actions
const toggleTodos = (): void => {
setStore("todosVisible", !store.todosVisible);
@@ -698,6 +758,7 @@ export const { provider: AppStoreProvider, use: useAppStore } =
streamingLogIsActive,
suggestions,
cascadeEnabled,
brain,
// Mode actions
setMode,
@@ -752,11 +813,19 @@ export const { provider: AppStoreProvider, use: useAppStore } =
setCascadeEnabled,
toggleCascadeEnabled,
// Brain actions
setBrainStatus,
setBrainUser,
setBrainCounts,
setBrainShowBanner,
dismissBrainBanner,
// Session stats actions
startThinking,
stopThinking,
addTokens,
resetSessionStats,
setContextMaxTokens,
// UI state actions
toggleTodos,
@@ -818,6 +887,13 @@ const defaultAppState = {
isCompacting: false,
streamingLog: createInitialStreamingState(),
suggestions: createInitialSuggestionState(),
brain: {
status: "disconnected" as BrainConnectionStatus,
user: null,
knowledgeCount: 0,
memoryCount: 0,
showBanner: true,
},
};
export const appStore = {
@@ -850,6 +926,7 @@ export const appStore = {
isCompacting: storeRef.isCompacting(),
streamingLog: storeRef.streamingLog(),
suggestions: storeRef.suggestions(),
brain: storeRef.brain(),
};
},
@@ -958,6 +1035,11 @@ export const appStore = {
storeRef.resetSessionStats();
},
setContextMaxTokens: (maxTokens: number): void => {
if (!storeRef) return;
storeRef.setContextMaxTokens(maxTokens);
},
toggleTodos: (): void => {
if (!storeRef) return;
storeRef.toggleTodos();
@@ -1027,4 +1109,29 @@ export const appStore = {
if (!storeRef) return;
storeRef.toggleCascadeEnabled();
},
setBrainStatus: (status: BrainConnectionStatus): void => {
if (!storeRef) return;
storeRef.setBrainStatus(status);
},
setBrainUser: (user: BrainUser | null): void => {
if (!storeRef) return;
storeRef.setBrainUser(user);
},
setBrainCounts: (knowledge: number, memory: number): void => {
if (!storeRef) return;
storeRef.setBrainCounts(knowledge, memory);
},
setBrainShowBanner: (show: boolean): void => {
if (!storeRef) return;
storeRef.setBrainShowBanner(show);
},
dismissBrainBanner: (): void => {
if (!storeRef) return;
storeRef.dismissBrainBanner();
},
};

View File

@@ -53,8 +53,7 @@ export function Home(props: HomeProps) {
>
<Logo />
<box marginTop={2} flexDirection="column" alignItems="center">
<text fg={theme.colors.textDim}>{HOME_VARS.title}</text>
<box marginTop={2}>
<text fg={theme.colors.textDim}>{HOME_VARS.subTitle}</text>
</box>
</box>

View File

@@ -21,6 +21,8 @@ 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 { BRAIN_DISABLED } from "@constants/brain";
import type { PermissionScope, LearningScope, InteractionMode } from "@/types/tui";
import type { MCPAddFormData } from "@/types/mcp";
@@ -60,6 +62,9 @@ interface SessionProps {
scope?: LearningScope,
editedContent?: string,
) => void;
onBrainSetJwtToken?: (jwtToken: string) => Promise<void>;
onBrainSetApiKey?: (apiKey: string) => Promise<void>;
onBrainLogout?: () => Promise<void>;
plan?: {
id: string;
title: string;
@@ -113,6 +118,10 @@ export function Session(props: SessionProps) {
app.transitionFromCommandMenu("help_menu");
return;
}
if (lowerCommand === "brain" && !BRAIN_DISABLED) {
app.transitionFromCommandMenu("brain_menu");
return;
}
// For other commands, close menu and process through handler
app.closeCommandMenu();
props.onCommand(command);
@@ -192,6 +201,22 @@ export function Session(props: SessionProps) {
app.setMode("idle");
};
const handleBrainMenuClose = (): void => {
app.setMode("idle");
};
const handleBrainSetJwtToken = async (jwtToken: string): Promise<void> => {
await props.onBrainSetJwtToken?.(jwtToken);
};
const handleBrainSetApiKey = async (apiKey: string): Promise<void> => {
await props.onBrainSetApiKey?.(apiKey);
};
const handleBrainLogout = async (): Promise<void> => {
await props.onBrainLogout?.();
};
return (
<box
flexDirection="column"
@@ -362,6 +387,18 @@ export function Session(props: SessionProps) {
/>
</CenteredModal>
</Match>
<Match when={app.mode() === "brain_menu" && !BRAIN_DISABLED}>
<CenteredModal>
<BrainMenu
onSetJwtToken={handleBrainSetJwtToken}
onSetApiKey={handleBrainSetApiKey}
onLogout={handleBrainLogout}
onClose={handleBrainMenuClose}
isActive={app.mode() === "brain_menu"}
/>
</CenteredModal>
</Match>
</Switch>
</box>
);