diff --git a/apps/supercode-cli/server/package.json b/apps/supercode-cli/server/package.json index 2cc97cc..c6e5610 100644 --- a/apps/supercode-cli/server/package.json +++ b/apps/supercode-cli/server/package.json @@ -1,6 +1,6 @@ { "name": "supercode-cli", - "version": "0.1.36", + "version": "0.1.38", "description": "AI-powered coding agent CLI", "main": "dist/main.js", "bin": { diff --git a/apps/supercode-cli/server/src/cli/ai/chat/chat.ts b/apps/supercode-cli/server/src/cli/ai/chat/chat.ts index 98957f6..766f03e 100644 --- a/apps/supercode-cli/server/src/cli/ai/chat/chat.ts +++ b/apps/supercode-cli/server/src/cli/ai/chat/chat.ts @@ -733,33 +733,32 @@ function renderInput() { process.stdout.write(promptText() + stdinInput) stdinPrevWrapLines = wrapLines - // Show slash list when input is exactly / + // Show slash autocomplete list slashListLines = 0 if (stdinInput.startsWith("/") && stdinInput.length >= 1) { const filtered = filterSlashCommands(stdinInput) - // Clamp selection to the filtered list length so we don't point past it - if (slashSelected >= filtered.length) slashSelected = -1 - const divider = heavyDivider() - process.stdout.write(`\r\n${divider}\r\n`) - if (slashSelected === -1) { - process.stdout.write(` ${chalk.hex(theme.amber)("❯")} ${stdinInput}\r\n`) - } - process.stdout.write(`${divider}\r\n`) - filtered.forEach((c, i) => { - if (slashSelected === i) { - process.stdout.write(` ${chalk.hex(theme.amber)("▸")} ${chalk.hex(theme.green).bold(c.cmd.padEnd(22))}${chalk.hex(theme.white)(c.desc)}\r\n`) - } else { - process.stdout.write(` ${chalk.hex(theme.muted)(" ")} ${chalk.hex(theme.green)(c.cmd.padEnd(22))}${chalk.hex(theme.muted)(c.desc)}\r\n`) + if (filtered.length > 0) { + if (slashSelected >= filtered.length) slashSelected = -1 + const divider = heavyDivider() + process.stdout.write(`\r\n${divider}\r\n`) + if (slashSelected === -1) { + process.stdout.write(` ${chalk.hex(theme.amber)("❯")} ${stdinInput}\r\n`) } - }) - process.stdout.write(`${divider}`) - // height: dividers (2) + header (1) + N commands + bottom divider (1) - selected line (1 if selected) - const slashHeight = filtered.length === 0 ? 0 : 2 + 1 + filtered.length + 1 - (slashSelected >= 0 ? 1 : 0) - slashListLines = slashHeight + process.stdout.write(`${divider}\r\n`) + filtered.forEach((c, i) => { + if (slashSelected === i) { + process.stdout.write(` ${chalk.hex(theme.amber)("▸")} ${chalk.hex(theme.green).bold(c.cmd.padEnd(22))}${chalk.hex(theme.white)(c.desc)}\r\n`) + } else { + process.stdout.write(` ${chalk.hex(theme.muted)(" ")} ${chalk.hex(theme.green)(c.cmd.padEnd(22))}${chalk.hex(theme.muted)(c.desc)}\r\n`) + } + }) + process.stdout.write(`${divider}`) + const slashHeight = 2 + 1 + filtered.length + 1 - (slashSelected >= 0 ? 1 : 0) + slashListLines = slashHeight - // Move cursor back up past the list to the input line - for (let i = 0; i < slashListLines; i++) { - readline.moveCursor(process.stdout, 0, -1) + for (let i = 0; i < slashListLines; i++) { + readline.moveCursor(process.stdout, 0, -1) + } } } @@ -815,7 +814,7 @@ function stdinKeypress(_str: string, key: any) { } if (key.name === "return" || key.name === "enter") { - // If a slash command is selected via keyboard, execute it directly + // If a slash command is selected, insert it so the user can type arguments const filtered = filterSlashCommands(stdinInput) if (slashSelected >= 0 && slashSelected < filtered.length) { const cmd = filtered[slashSelected]!.cmd @@ -832,10 +831,9 @@ function stdinKeypress(_str: string, key: any) { } } slashListLines = 0 - process.stdout.write("\r\n") - const resolve = stdinResolve - stdinResolve = null - resolve({ input: cmd, mode: stdinMode }) + stdinInput = cmd + " " + stdinCursor = stdinInput.length + renderInput() return } @@ -1455,6 +1453,23 @@ export async function chatLoop( ) } else if (result?.type === "unknown") { process.stdout.write(`\r\n ${chalk.hex(theme.red)("◆")} unknown slash command: ${trimmed.split(" ")[0]}\r\n\n`) + } else if (result?.type === "message" && result.message) { + userMessage(trimmed) + messageCount++ + await addMessage(conversation.id, "user", result.message) + await trySetAutoTitle(conversation.id, result.message, messageCount) + try { + const aiResult = await streamAIResponse(provider, conversation.id, conversation.mode, workspaceInfo, footer) + if (aiResult.aborted) { + process.stdout.write(` ${chalk.hex(theme.muted)("response aborted")}\r\n\n`) + } + footer.render() + } catch (err: any) { + const msg = err.message || String(err) + process.stdout.write(`\r\n ${chalk.hex(theme.red)("◆")} ${chalk.hex(theme.red)(msg)}\r\n\n`) + } + readline.cursorTo(process.stdout, 0) + continue } else { process.stdout.write("\r\n") } diff --git a/apps/supercode-cli/server/src/cli/commands/slashCommands/index.ts b/apps/supercode-cli/server/src/cli/commands/slashCommands/index.ts index 9d6f9b7..3c59a04 100644 --- a/apps/supercode-cli/server/src/cli/commands/slashCommands/index.ts +++ b/apps/supercode-cli/server/src/cli/commands/slashCommands/index.ts @@ -3,15 +3,15 @@ import chalk from "chalk" import { pickModel, formatModelChange } from "./model.ts" import { connectProvider } from "./connect.ts" import { renderHelp } from "./help.ts" -import { searchSlash, scrapeSlash, interactSlash, crawlSlash, parseSlash } from "./firecrawl.ts" import { theme, heavyDivider } from "src/cli/utils/tui.ts" import type { ModelProvider } from "src/cli/ai/provider.ts" export interface SlashCommandResult { - type: "model_change" | "help" | "unknown" | "exit" | "connect" | "context" | "compact" | "plan" | "scratch" | "voice" | "verbose" + type: "model_change" | "help" | "unknown" | "exit" | "connect" | "context" | "compact" | "plan" | "scratch" | "voice" | "verbose" | "message" provider?: ModelProvider model?: string label?: string + message?: string } export const COMMANDS = [ @@ -32,6 +32,18 @@ export const COMMANDS = [ { cmd: "/exit", desc: "End the session" }, ] +function chatify(cmd: string, args: string): string { + const messages: Record = { + search: `Search the web`, + scrape: `Scrape a URL to extract markdown content`, + interact: `Perform a browser interaction`, + crawl: `Crawl a website`, + parse: `Parse a document file`, + } + const base = messages[cmd] || cmd + return args ? `${base} for: ${args}` : base +} + const handlers: Record Promise> = { model: async () => { const result = await pickModel() @@ -71,11 +83,11 @@ const handlers: Record Promise> = verbose: async () => { return { type: "verbose" } }, - search: async (args) => searchSlash(args), - scrape: async (args) => scrapeSlash(args), - interact: async (args) => interactSlash(args), - crawl: async (args) => crawlSlash(args), - parse: async (args) => parseSlash(args), + search: async (args) => ({ type: "message", message: chatify("search", args) }), + scrape: async (args) => ({ type: "message", message: chatify("scrape", args) }), + interact: async (args) => ({ type: "message", message: chatify("interact", args) }), + crawl: async (args) => ({ type: "message", message: chatify("crawl", args) }), + parse: async (args) => ({ type: "message", message: chatify("parse", args) }), } function renderCommandList(): void { diff --git a/apps/supercode-cli/server/src/index.ts b/apps/supercode-cli/server/src/index.ts index 3b36eb4..63a37b8 100644 --- a/apps/supercode-cli/server/src/index.ts +++ b/apps/supercode-cli/server/src/index.ts @@ -3,10 +3,13 @@ import { toNodeHandler } from "better-auth/node" import { auth } from "./lib/auth" import cors from "cors" import prisma from "./lib/prisma" +import { loadEnvOnce } from "./lib/load-env" import { recordUsage } from "./lib/track-usage" import { computeCost } from "./lib/pricing" import { registerAnalyticsRoutes } from "./routes/analytics" +loadEnvOnce() + const port = process.env.PORT || 10000 const serverUrl = process.env.BETTER_AUTH_URL || `http://localhost:${port}` const clientUrl = process.env.CLIENT_URL || "http://localhost:3000"