diff --git a/.env b/.env index 90f8ab61..a993988c 100644 --- a/.env +++ b/.env @@ -1,5 +1,10 @@ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://otel-collector:4318/v1/logs OTEL_SERVICE_NAME=sup-agent -LOG_LEVEL=info - +LOG_LEVEL=warn LOKI_HOST=http://localhost:3100 + + +SUPPORT_AGENT_MODEL=qwen3:8b + + + diff --git a/apps/cli/chat.ts b/apps/cli/chat.ts index fcb0b50a..20bbd87b 100644 --- a/apps/cli/chat.ts +++ b/apps/cli/chat.ts @@ -68,6 +68,9 @@ export async function startChat() { events: [], }; + console.log(` Active Agent: \x1b[1m${AGENT.toUpperCase()}\x1b[0m`); + console.log(` Active Model: \x1b[32m${typeof modelSpec === 'object' ? JSON.stringify(modelSpec) : modelSpec}\x1b[0m`); + process.nextTick(() => rl.prompt()); // logger.debug(`Agent: ${AGENT}`); //logger.debug(`Model: ${modelSpec}`); diff --git a/apps/tui/index.ts b/apps/tui/index.ts index bdb5a872..0baff2a7 100644 --- a/apps/tui/index.ts +++ b/apps/tui/index.ts @@ -1,6 +1,5 @@ import { render } from "@opentui/solid"; -import { createSignal } from "solid-js"; -import { For } from "solid-js"; +import { createSignal, For, onMount } from "solid-js"; import { OpenFeature, MultiProvider, @@ -11,70 +10,92 @@ import { ProtocolResolver } from "@sup/lib/protocol-resolver"; import { adapters } from "@sup/tools"; import { JsonFileProvider } from "@sup/infra/adapters/JsonFileProvider"; -const AGENT = process.env.AGENT || "support"; -const { agent, modelSpec } = await (async () => { - if (AGENT === "coding") { - const { codingAgent, codingAgentModelSpec } = await import("@sup/agents/coding-agent"); - return { agent: codingAgent, modelSpec: codingAgentModelSpec }; - } - const { supportAgent, supportAgentModelSpec } = await import("@sup/agents/support-agent"); - return { agent: supportAgent, modelSpec: supportAgentModelSpec }; -})(); - -/* -const providers = []; -if (process.env.POSTHOG_API_KEY) { - const { PostHogProvider } = await import("@tapico/node-openfeature-posthog"); - const { PostHog } = await import("posthog-node"); - const posthogClient = new PostHog(process.env.POSTHOG_API_KEY, { - host: process.env.POSTHOG_HOST || "https://app.posthog.com", - }); - providers.push({ provider: new PostHogProvider({ posthogClient }) }); -} -providers.push({ provider: new JsonFileProvider("../../config/flags.json") }); -*/ - -const multiProvider = new MultiProvider(providers, new FirstSuccessfulStrategy()); -await OpenFeature.setProviderAndWait(multiProvider); - -const session: AgentSession = { - id: "tui-session-" + Date.now(), - events: [], +type AgentSession = { + id: string; + events: any[]; }; type Message = { role: "user" | "agent"; text: string }; +const AGENT = process.env.AGENT || "support"; + function App() { const [messages, setMessages] = createSignal([]); const [streaming, setStreaming] = createSignal(""); const [activeTool, setActiveTool] = createSignal(""); const [busy, setBusy] = createSignal(false); + const [ready, setReady] = createSignal(false); + + // Keep a reference to the active agent function + let currentAgent: any = null; + + onMount(async () => { + try { + // 1. Setup OpenFeature without stalling root script + const providers = []; + if (process.env.POSTHOG_API_KEY) { + const { PostHogProvider } = await import("@tapico/node-openfeature-posthog"); + const { PostHog } = await import("posthog-node"); + const posthogClient = new PostHog(process.env.POSTHOG_API_KEY, { + host: process.env.POSTHOG_HOST || "https://app.posthog.com", + }); + providers.push({ provider: new PostHogProvider({ posthogClient }) }); + } + providers.push({ provider: new JsonFileProvider("../../config/flags.json") }); + + const multiProvider = new MultiProvider(providers, new FirstSuccessfulStrategy()); + await OpenFeature.setProviderAndWait(multiProvider); + + // 2. Resolve Agent inside onMount so OpenTUI doesn't lock up module loading + if (AGENT === "coding") { + const { codingAgent } = await import("@sup/agents/coding-agent"); + currentAgent = codingAgent; + } else { + const { supportAgent } = await import("@sup/agents/support-agent"); + currentAgent = supportAgent; + } + + setReady(true); + } catch (e) { + console.error("Initialization error:", e); + setReady(true); // Fallback so UI still draws + } + }); + + const session: AgentSession = { + id: "tui-session-" + Date.now(), + events: [], + }; async function submit(text: string) { - if (!text.trim() || busy()) return; + if (!text.trim() || busy() || !ready() || !currentAgent) return; setBusy(true); setMessages((m) => [...m, { role: "user", text }]); - const generator = agent(text, session, { - resolver: ProtocolResolver, - tools: adapters, - }); - - for await (const step of generator) { - if (step.type === "text_delta" && step.delta) { - setStreaming((s) => s + step.delta); - } else if (step.type === "tool_call") { - setActiveTool(step.toolId); - } else if (step.type === "tool_result") { - setActiveTool(""); - } else if (step.type === "final") { - setMessages((m) => [ - ...m, - { role: "agent", text: step.text || streaming() }, - ]); - setStreaming(""); - setActiveTool(""); + try { + const generator = currentAgent(text, session, { + resolver: ProtocolResolver, + tools: adapters, + }); + + for await (const step of generator) { + if (step.type === "text_delta" && step.delta) { + setStreaming((s) => s + step.delta); + } else if (step.type === "tool_call") { + setActiveTool(step.toolId); + } else if (step.type === "tool_result") { + setActiveTool(""); + } else if (step.type === "final") { + setMessages((m) => [ + ...m, + { role: "agent", text: step.text || streaming() }, + ]); + setStreaming(""); + setActiveTool(""); + } } + } catch (err) { + setMessages((m) => [...m, { role: "agent", text: `Error: ${err}` }]); } setBusy(false); @@ -90,7 +111,6 @@ function App() { {msg.role === "user" ? "You" : "Agent"}:{" "} {msg.text} -
)} @@ -108,8 +128,8 @@ function App() { @@ -118,4 +138,4 @@ function App() { ); } -render(() => ); +render(() => ); \ No newline at end of file diff --git a/apps/tui/index.tsx b/apps/tui/index.tsx index 0850eb97..12946c00 100644 --- a/apps/tui/index.tsx +++ b/apps/tui/index.tsx @@ -1,6 +1,6 @@ +process.stdout.write = process.stderr.write.bind(process.stderr); import { render } from "@opentui/solid"; -import { createSignal } from "solid-js"; -import { For } from "solid-js"; +import { createSignal, For, onMount } from "solid-js"; import { OpenFeature, MultiProvider, @@ -11,106 +11,184 @@ import { ProtocolResolver } from "@sup/lib/protocol-resolver"; import { adapters } from "@sup/tools"; import { JsonFileProvider } from "@sup/infra/adapters/JsonFileProvider"; -const AGENT = process.env.AGENT || "support"; -const { agent, modelSpec } = await (async () => { - if (AGENT === "coding") { - const { codingAgent, codingAgentModelSpec } = await import("@sup/agents/coding-agent"); - return { agent: codingAgent, modelSpec: codingAgentModelSpec }; - } - const { supportAgent, supportAgentModelSpec } = await import("@sup/agents/support-agent"); - return { agent: supportAgent, modelSpec: supportAgentModelSpec }; -})(); - -/* -const providers = []; -if (process.env.POSTHOG_API_KEY) { - const { PostHogProvider } = await import("@tapico/node-openfeature-posthog"); - const { PostHog } = await import("posthog-node"); - const posthogClient = new PostHog(process.env.POSTHOG_API_KEY, { - host: process.env.POSTHOG_HOST || "https://app.posthog.com", - }); - providers.push({ provider: new PostHogProvider({ posthogClient }) }); -} -providers.push({ provider: new JsonFileProvider("../../config/flags.json") }); - - -const multiProvider = new MultiProvider(providers, new FirstSuccessfulStrategy()); -await OpenFeature.setProviderAndWait(multiProvider); -*/ - -const session: AgentSession = { - id: "tui-session-" + Date.now(), - events: [], +type AgentSession = { + id: string; + events: any[]; }; type Message = { role: "user" | "agent"; text: string }; +const AGENT = process.env.AGENT || "support"; + function App() { const [messages, setMessages] = createSignal([]); const [streaming, setStreaming] = createSignal(""); const [activeTool, setActiveTool] = createSignal(""); const [busy, setBusy] = createSignal(false); + const [ready, setReady] = createSignal(false); + const [currentModel, setCurrentModel] = createSignal("Detecting..."); + + let inputRef: any; + + let currentAgent: any = null; + + onMount(async () => { + try { + const providers = []; + if (process.env.POSTHOG_API_KEY) { + const { PostHogProvider } = await import("@tapico/node-openfeature-posthog"); + const { PostHog } = await import("posthog-node"); + const posthogClient = new PostHog(process.env.POSTHOG_API_KEY, { + host: process.env.POSTHOG_HOST || "https://app.posthog.com", + }); + providers.push({ provider: new PostHogProvider({ posthogClient }) }); + } + providers.push({ provider: new JsonFileProvider("../../config/flags.json") }); + + const multiProvider = new MultiProvider(providers, new FirstSuccessfulStrategy()); + await OpenFeature.setProviderAndWait(multiProvider); + + if (AGENT === "coding") { + const { codingAgent, codingAgentModelSpec } = await import("@sup/agents/coding-agent"); + currentAgent = codingAgent; + // Extracted structural metadata details safely from imports + setCurrentModel(codingAgentModelSpec); + } else { + const { supportAgent, supportAgentModelSpec } = await import("@sup/agents/support-agent"); + currentAgent = supportAgent; + setCurrentModel(supportAgentModelSpec?.name); + } + + setReady(true); + setTimeout(() => inputRef?.focus(), 0); + } catch (e) { + setCurrentModel("Fallback-LLM"); + setReady(true); + } + }); + + const session: AgentSession = { + id: "tui-session-" + Date.now(), + events: [], + }; async function submit(text: string) { - if (!text.trim() || busy()) return; + if (!text.trim() || busy() || !ready() || !currentAgent) return; setBusy(true); setMessages((m) => [...m, { role: "user", text }]); - const generator = agent(text, session, { - resolver: ProtocolResolver, - tools: adapters, - }); - - for await (const step of generator) { - if (step.type === "text_delta" && step.delta) { - setStreaming((s) => s + step.delta); - } else if (step.type === "tool_call") { - setActiveTool(step.toolId); - } else if (step.type === "tool_result") { - setActiveTool(""); - } else if (step.type === "final") { - setMessages((m) => [ - ...m, - { role: "agent", text: step.text || streaming() }, - ]); - setStreaming(""); - setActiveTool(""); + try { + const generator = currentAgent(text, session, { + resolver: ProtocolResolver, + tools: adapters, + }); + + for await (const step of generator) { + if (step.type === "text_delta" && step.delta) { + setStreaming((s) => s + step.delta); + } else if (step.type === "tool_call") { + setActiveTool(step.toolId); + } else if (step.type === "tool_result") { + setActiveTool(""); + } else if (step.type === "final") { + setMessages((m) => [ + ...m, + { role: "agent", text: step.text || streaming() }, + ]); + setStreaming(""); + setActiveTool(""); + } } + } catch (err) { + setMessages((m) => [...m, { role: "agent", text: `Error: ${err}` }]); } setBusy(false); } return ( - - - - {(msg) => ( - - - {msg.role === "user" ? "You" : "Agent"}:{" "} - - {msg.text} -
-
+ + + {/* 1. Global App Header */} + + SUP // + WORKSPACE CONTROL PANEL + + + {ready() ? "● ACTIVE CONTEXT" : "○ SYNCING ENVIRONMENT"} + + + + {/* 2. Main Middle Deck (Splits layout horizontally) */} + + + {/* Left Side: Scrollable Conversation Field */} + + + {(msg) => ( + + + {msg.role === "user" ? "❯ You" : "❯ Agent"} + + + {msg.text} + + + )} + + + {streaming() && ( + + ❯ Agent + + {streaming()} + + )} -
- {streaming() && ( - - Agent: - {streaming()} - - )} - {activeTool() && ( - - [{activeTool()}] +
+ + {/* Right Side: Sidebar Meta Deck Panel */} + + [ AGENT SPEC ] + + Type: + {AGENT.toUpperCase()} + + Engine Model: + {currentModel()} + + + {busy() ? "⚡ Processing" : "💤 Idle"} - )} - - + + Telemetry Log: + {activeTool() ? ( + + » Tool Invoked + [{activeTool()}] + + ) : ( + Listening... + )} + + + + {/* 3. Action Input Base Footer */} + + {busy() ? " ⧗ " : " ❯ "} @@ -119,4 +197,4 @@ function App() { ); } -render(() => ); +render(() => ); \ No newline at end of file diff --git a/bar.txt b/bar.txt deleted file mode 100644 index 274ced28..00000000 --- a/bar.txt +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/baz.txt b/baz.txt deleted file mode 100644 index dc0a6fe4..00000000 --- a/baz.txt +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/baz2.txt b/baz2.txt deleted file mode 100644 index b1b0da41..00000000 --- a/baz2.txt +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/baz3.txt b/baz3.txt deleted file mode 100644 index d9f9cb1e..00000000 --- a/baz3.txt +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/foo.txt b/foo.txt deleted file mode 100644 index 4b37d937..00000000 --- a/foo.txt +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/hello.txt b/hello.txt deleted file mode 100644 index 5e1c309d..00000000 --- a/hello.txt +++ /dev/null @@ -1 +0,0 @@ -Hello World \ No newline at end of file diff --git a/list-models.py b/list-models.py new file mode 100644 index 00000000..400691e6 --- /dev/null +++ b/list-models.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +import subprocess +import re + +def get_ollama_models(): + try: + # Run the 'ollama list' command + result = subprocess.run(['ollama', 'list'], capture_output=True, text=True, check=True) + lines = result.stdout.strip().split('\n') + if len(lines) <= 1: + return [] + + models = [] + # Skip the header line + for line in lines[1:]: + # Use regex to handle variable whitespace spacing + parts = re.split(r'\s{2,}', line.strip()) + if len(parts) >= 4: + # Handle cases where ID, Size, or Modified columns might group slightly differently + name = parts[0] + model_id = parts[1] + size = parts[2] + # Combine remaining parts if 'Modified' contains spaces (e.g., "5 weeks ago") + modified = " ".join(parts[3:]) + + # Determine thinking capability + thinking = "No" + name_lower = name.lower() + if "qwen3" in name_lower: + thinking = "Hybrid (Native Switchable)" + elif "deepseek-r1" in name_lower or "-r1" in name_lower or "reasoning" in name_lower: + thinking = "Yes (Reasoning/CoT Model)" + elif "coder" in name_lower: + thinking = "No (Code LLM)" + elif "embed" in name_lower: + thinking = "No (Embedding Model)" + elif "agent" in name_lower: + thinking = "No (Agentic LLM)" + else: + thinking = "No" + + models.append([name, model_id, size, modified, thinking]) + return models + except FileNotFoundError: + print("Error: 'ollama' command not found. Make sure Ollama is installed and in your PATH.") + return None + except subprocess.CalledProcessError: + print("Error: Failed to execute 'ollama list'. Is the Ollama service running?") + return None + +def print_ascii_table(models): + if not models: + print("No local Ollama models found.") + return + + headers = ["NAME", "ID", "SIZE", "MODIFIED", "THINKING MODEL?"] + + # Calculate dynamic column widths based on longest content + col_widths = [len(h) for h in headers] + for row in models: + for i in range(5): + if len(row[i]) > col_widths[i]: + col_widths[i] = len(row[i]) + + # Build the separator line: +-----+----+ + sep = "+" + "+".join(["-" * (w + 2) for w in col_widths]) + "+" + + # Print table + print(sep) + header_row = "| " + " | ".join([f"{headers[i]:<{col_widths[i]}}" for i in range(5)]) + " |" + print(header_row) + print(sep) + + for row in models: + model_row = "| " + " | ".join([f"{row[i]:<{col_widths[i]}}" for i in range(5)]) + " |" + print(model_row) + + print(sep) + +if __name__ == "__main__": + model_data = get_ollama_models() + if model_data is not None: + print_ascii_table(model_data) diff --git a/models.txt b/models.txt new file mode 100644 index 00000000..2e70d719 --- /dev/null +++ b/models.txt @@ -0,0 +1,14 @@ ++------------------------------+--------------+---------+--------------+-----------------------------+ +| NAME | ID | SIZE | MODIFIED | THINKING MODEL? | ++------------------------------+--------------+---------+--------------+-----------------------------+ +| opencode-qwen:latest | 3cf2fe22f8e4 | 4.7 GB | 5 weeks ago | No (Standard/Code LLM) | +| qwen2.5-coder-agent:latest | fa2f24e4d32b | 4.7 GB | 5 weeks ago | No (Agentic/Code LLM) | +| qwen3:8b | 500a1f067a9f | 5.2 GB | 3 months ago | Hybrid (Native Switchable) | +| qwen2.5:7b | 845dbda0ea48 | 4.7 GB | 4 months ago | No (General LLM) | +| orieg/gemma3-tools:4b-it-qat | 112b9b93b581 | 4.0 GB | 4 months ago | No (Tool-use LLM) | +| qwen2.5-coder:1.5b | d7372fd82851 | 986 MB | 9 months ago | No (Code LLM) | +| qwen2.5-coder:7b | dae161e27b0e | 4.7 GB | 9 months ago | No (Code LLM) | +| deepseek-r1:latest | 6995872bfe4c | 5.2 GB | 9 months ago | Yes (Reasoning/CoT Model) | +| nomic-embed-text:latest | 0a109f422b47 | 274 MB | 10 mos ago | No (Embedding Model) | +| gemma3:latest | a2af6cc3eb7f | 3.3 GB | 10 mos ago | No (General LLM) | ++------------------------------+--------------+---------+--------------+-----------------------------+ diff --git a/package.json b/package.json index 857cad01..55fe9cbd 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "sup:loki": "AGENT=support LOKI_ENABLED=true bun apps/cli/chat.ts", "coda": "AGENT=coding bun apps/cli/chat.ts", "coda:cli": "AGENT=coding bun apps/cli/chat.ts", - "coda:tui": "AGENT=coding bun --env-file=.auth apps/tui/index.tsx", + "tui:debug": "AGENT=coding bun --env-file=.auth apps/tui/index.tsx", + "coda:tui": "AGENT=coding bun --env-file=.auth apps/tui/index.tsx 2>/dev/null", "lint": "eslint . --cache --max-warnings 0", "lint:fix": "eslint . --cache --fix --max-warnings 0", "preview": "vite preview", diff --git a/packages/agents/coding-agent.ts b/packages/agents/coding-agent.ts index fee46116..d4714cc9 100644 --- a/packages/agents/coding-agent.ts +++ b/packages/agents/coding-agent.ts @@ -352,4 +352,5 @@ export async function* codingAgent( yield { type: 'final', timestamp: Date.now(), text: bestText(summaryText, summaryReasoning) }; } -export const codingAgentModelSpec = `router:${ROUTER_MODEL} coder:${CODER_MODEL}`; \ No newline at end of file +export const codingAgentModelSpec = `router:${ROUTER_MODEL} +coder:${CODER_MODEL}`; \ No newline at end of file diff --git a/packages/infra/env.ts b/packages/infra/env.ts index 895f641a..0449ee44 100644 --- a/packages/infra/env.ts +++ b/packages/infra/env.ts @@ -2,9 +2,12 @@ * @file infra/env.ts * @description Load environment first */ -const DEFAULT_MODEL = 'qwen3:8b'; + const DEFAULT_OWNER = 'ForestMars'; const DEFAULT_REPO = 'coffee'; +const DEFAULT_MODEL = 'qwen3:8b'; +const DEFAULT_CODING_MODEL = 'qwen3:8b'; +const DEFAULT_SUPPORT_AGENT_MODEL = 'qwen3:8b'; export const FACTOTUM_MODEL = 'qwen2.5:1.5b'; export const TEMPERATURE = 0; diff --git a/packages/infra/logger/index.ts b/packages/infra/logger/index.ts index 976e9a1b..a62b7f5b 100644 --- a/packages/infra/logger/index.ts +++ b/packages/infra/logger/index.ts @@ -14,7 +14,7 @@ export const pinoStorage = new AsyncLocalStorage< >(); const MODEL_NAME = AGENT_MODEL -const isTerminal = process.stdout.isTTY; +const isTerminal = process.stdout.isTTY && !process.env.TUI_MODE; const isDev = process.env.NODE_ENV !== 'production'; const lokiEnabled = process.env.LOKI_ENABLED === 'true'; @@ -37,7 +37,7 @@ if (isTerminal && isDev) { streams.push({ stream: pretty, level: 'debug' }); } else { streams.push({ - stream: pino.destination(2), + stream: pino.destination('/dev/null'), level: 'info', }); }