arcie 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -149,9 +149,20 @@ function createCliProgram(logger) {
149
149
  });
150
150
  return program;
151
151
  }
152
+ var KNOWN_COMMANDS = /* @__PURE__ */ new Set(["channels", "init", "build", "dev", "help"]);
153
+ function resolveArgv(argv) {
154
+ if (argv.length === 0) return ["dev"];
155
+ const first = argv[0];
156
+ if (first === "-h" || first === "--help" || first === "-V" || first === "--version") {
157
+ return [...argv];
158
+ }
159
+ if (first.startsWith("-")) return [...argv];
160
+ if (KNOWN_COMMANDS.has(first)) return [...argv];
161
+ return ["init", ...argv];
162
+ }
152
163
  async function runCli(argv = process.argv.slice(2), logger = console) {
153
164
  const program = createCliProgram(logger);
154
- const input = argv.length === 0 ? ["dev"] : argv;
165
+ const input = resolveArgv(argv);
155
166
  try {
156
167
  await program.parseAsync(input, { from: "user" });
157
168
  } catch (error) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { resolve as resolvePath } from \"node:path\";\nimport { Command, CommanderError, InvalidArgumentError } from \"commander\";\nimport { arcieCliBanner, version } from \"./banner\";\nimport { createCliTheme, renderCliTaggedLine } from \"./ui/output\";\n\ninterface CliLogger {\n error(message: string): void;\n log(message: string): void;\n}\n\nconst DISPLAY_MODES = new Set([\"full\", \"collapsed\", \"auto-collapsed\", \"hidden\"]);\nconst STATS_MODES = new Set([\"tokens\", \"tokensPerSecond\"]);\nconst LOG_MODES = new Set([\"all\", \"stderr\", \"none\"]);\n\ntype TerminalPartDisplayMode = \"full\" | \"collapsed\" | \"auto-collapsed\" | \"hidden\";\ntype AssistantResponseStatsMode = \"tokens\" | \"tokensPerSecond\";\ntype LogDisplayMode = \"all\" | \"stderr\" | \"none\";\n\nfunction parsePortOption(value: string): number {\n if (!/^-?\\d+$/.test(value)) {\n throw new InvalidArgumentError(`Expected a numeric port, received \"${value}\".`);\n }\n const port = Number(value);\n if (port < 0 || port > 65_535) {\n throw new InvalidArgumentError(`Expected a port between 0 and 65535, received \"${value}\".`);\n }\n return port;\n}\n\nfunction parseDisplayMode(value: string): TerminalPartDisplayMode {\n if (!DISPLAY_MODES.has(value)) {\n throw new InvalidArgumentError(\n `Expected one of ${[...DISPLAY_MODES].join(\", \")}, received \"${value}\".`,\n );\n }\n return value as TerminalPartDisplayMode;\n}\n\nfunction parseStatsMode(value: string): AssistantResponseStatsMode {\n if (!STATS_MODES.has(value)) {\n throw new InvalidArgumentError(\n `Expected one of ${[...STATS_MODES].join(\", \")}, received \"${value}\".`,\n );\n }\n return value as AssistantResponseStatsMode;\n}\n\nfunction parseLogsMode(value: string): LogDisplayMode {\n if (!LOG_MODES.has(value)) {\n throw new InvalidArgumentError(\n `Expected one of ${[...LOG_MODES].join(\", \")}, received \"${value}\".`,\n );\n }\n return value as LogDisplayMode;\n}\n\nfunction parseContextSizeOption(value: string): number {\n const size = Number(value);\n if (!Number.isFinite(size) || size <= 0) {\n throw new InvalidArgumentError(`Expected a positive number, received \"${value}\".`);\n }\n return size;\n}\n\nfunction shouldPrintCliBootBanner(actionCommand: Command): boolean {\n const name = actionCommand.name();\n return name === \"info\" || name === \"dev\" || name === \"init\";\n}\n\nfunction createCliProgram(logger: CliLogger): Command {\n const program = new Command();\n const theme = createCliTheme();\n\n program\n .name(\"arcie\")\n .description(\"arcie — the electronic line, build agents at the speed of light\")\n .version(version())\n .showHelpAfterError()\n .exitOverride()\n .hook(\"preAction\", (_program, actionCommand) => {\n if (shouldPrintCliBootBanner(actionCommand)) {\n logger.log(arcieCliBanner());\n }\n })\n .configureOutput({\n writeErr: (message) => logger.error(message.trimEnd()),\n writeOut: (message) => logger.log(message.trimEnd()),\n });\n\n const channels = program\n .command(\"channels\")\n .description(\"Manage channels for the current agent (web UI, HTTP, etc.).\");\n\n channels\n .command(\"add <kind>\")\n .description(\"Scaffold a channel (currently supports: web).\")\n .option(\"--agent-dir <path>\", \"Path to agent directory\", \".\")\n .action(async (kind: string, options: { agentDir: string }) => {\n if (kind !== \"web\") {\n logger.error(`unknown channel kind: ${kind}. supported: web`);\n process.exit(1);\n }\n const agentDir = resolvePath(process.cwd(), options.agentDir);\n const { scaffoldWebChat } = await import(\"./scaffold-web-chat\");\n try {\n const result = scaffoldWebChat(agentDir);\n if (result.alreadyExisted) {\n logger.error(`channels/web already exists at ${result.targetPath}`);\n process.exit(1);\n }\n logger.log(\n renderCliTaggedLine(theme, {\n message: `scaffolded ${result.targetPath}`,\n tag: \"channels\",\n tone: \"success\",\n }),\n );\n logger.log(\n renderCliTaggedLine(theme, {\n message: \"cd into it, then: npm install && cp .env.local.example .env.local && npm run dev\",\n tag: \"next\",\n tone: \"info\",\n }),\n );\n } catch (err) {\n logger.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n });\n\n program\n .command(\"init [target]\")\n .description(\"Create a new arcie agent, or add one to an existing project directory.\")\n .option(\"--template <name>\", \"Template to use (default, agent-only)\", \"default\")\n .option(\"-y, --yes\", \"Accepted for compatibility; has no effect\")\n .action(async (target: string | undefined, options: { template: string; yes?: boolean }) => {\n if (options.yes) logger.error(\"warning: --yes has no effect for arcie init.\");\n const { initCommand } = await import(\"./init\");\n await initCommand(target, { template: options.template });\n });\n\n program\n .command(\"build\")\n .description(\"Compile the agent for production.\")\n .option(\"--agent-dir <path>\", \"Path to agent directory\", \"agent\")\n .option(\"--out-dir <path>\", \"Output directory\", \".arcie\")\n .action(async (options: { agentDir: string; outDir: string }) => {\n const { buildCommand } = await import(\"./build\");\n await buildCommand(options);\n logger.log(\n renderCliTaggedLine(theme, {\n message: `output at ${options.outDir}`,\n tag: \"build\",\n tone: \"success\",\n }),\n );\n });\n\n program\n .command(\"dev\")\n .description(\"Start the arcie development server or attach an interactive UI.\")\n .option(\"-p, --port <port>\", \"Port to listen on\", parsePortOption, 3000)\n .option(\"--host <host>\", \"Host interface to bind\")\n .option(\"--agent-dir <path>\", \"Path to agent directory\", \"agent\")\n .option(\"--input <text>\", \"Pre-fill the prompt input, or start onboarding with /model\")\n .option(\"--no-ui\", \"Start the server without an interactive UI\")\n .option(\"--name <name>\", \"Title shown in the terminal UI (defaults to the app folder name)\")\n .option(\n \"--tools <mode>\",\n \"How tool calls render: full | collapsed | auto-collapsed | hidden\",\n parseDisplayMode,\n )\n .option(\n \"--reasoning <mode>\",\n \"How reasoning renders: full | collapsed | auto-collapsed | hidden\",\n parseDisplayMode,\n )\n .option(\n \"--subagents <mode>\",\n \"How subagent sections render: full | collapsed | auto-collapsed | hidden\",\n parseDisplayMode,\n )\n .option(\n \"--assistant-response-stats <mode>\",\n \"Assistant header statistic: tokens | tokensPerSecond\",\n parseStatsMode,\n )\n .option(\n \"--context-size <tokens>\",\n \"Model context window size, shown as a usage percentage\",\n parseContextSizeOption,\n )\n .option(\"--logs <mode>\", \"Which logs to show: all | stderr | none\", parseLogsMode)\n .action(async (options) => {\n const { devCommand } = await import(\"./dev\");\n await devCommand({\n port: String(options.port ?? 3000),\n agentDir: options.agentDir,\n input: options.ui === false ? false : Boolean(options.input) || options.ui !== false,\n });\n });\n\n return program;\n}\n\nexport async function runCli(\n argv: string[] = process.argv.slice(2),\n logger: CliLogger = console,\n): Promise<void> {\n const program = createCliProgram(logger);\n const input = argv.length === 0 ? [\"dev\"] : argv;\n\n try {\n await program.parseAsync(input, { from: \"user\" });\n } catch (error) {\n if (error instanceof CommanderError) {\n if (error.exitCode === 0) return;\n throw new Error(error.message);\n }\n throw error;\n }\n}\n\nrunCli().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(message);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { resolve as resolvePath } from \"node:path\";\nimport { Command, CommanderError, InvalidArgumentError } from \"commander\";\nimport { arcieCliBanner, version } from \"./banner\";\nimport { createCliTheme, renderCliTaggedLine } from \"./ui/output\";\n\ninterface CliLogger {\n error(message: string): void;\n log(message: string): void;\n}\n\nconst DISPLAY_MODES = new Set([\"full\", \"collapsed\", \"auto-collapsed\", \"hidden\"]);\nconst STATS_MODES = new Set([\"tokens\", \"tokensPerSecond\"]);\nconst LOG_MODES = new Set([\"all\", \"stderr\", \"none\"]);\n\ntype TerminalPartDisplayMode = \"full\" | \"collapsed\" | \"auto-collapsed\" | \"hidden\";\ntype AssistantResponseStatsMode = \"tokens\" | \"tokensPerSecond\";\ntype LogDisplayMode = \"all\" | \"stderr\" | \"none\";\n\nfunction parsePortOption(value: string): number {\n if (!/^-?\\d+$/.test(value)) {\n throw new InvalidArgumentError(`Expected a numeric port, received \"${value}\".`);\n }\n const port = Number(value);\n if (port < 0 || port > 65_535) {\n throw new InvalidArgumentError(`Expected a port between 0 and 65535, received \"${value}\".`);\n }\n return port;\n}\n\nfunction parseDisplayMode(value: string): TerminalPartDisplayMode {\n if (!DISPLAY_MODES.has(value)) {\n throw new InvalidArgumentError(\n `Expected one of ${[...DISPLAY_MODES].join(\", \")}, received \"${value}\".`,\n );\n }\n return value as TerminalPartDisplayMode;\n}\n\nfunction parseStatsMode(value: string): AssistantResponseStatsMode {\n if (!STATS_MODES.has(value)) {\n throw new InvalidArgumentError(\n `Expected one of ${[...STATS_MODES].join(\", \")}, received \"${value}\".`,\n );\n }\n return value as AssistantResponseStatsMode;\n}\n\nfunction parseLogsMode(value: string): LogDisplayMode {\n if (!LOG_MODES.has(value)) {\n throw new InvalidArgumentError(\n `Expected one of ${[...LOG_MODES].join(\", \")}, received \"${value}\".`,\n );\n }\n return value as LogDisplayMode;\n}\n\nfunction parseContextSizeOption(value: string): number {\n const size = Number(value);\n if (!Number.isFinite(size) || size <= 0) {\n throw new InvalidArgumentError(`Expected a positive number, received \"${value}\".`);\n }\n return size;\n}\n\nfunction shouldPrintCliBootBanner(actionCommand: Command): boolean {\n const name = actionCommand.name();\n return name === \"info\" || name === \"dev\" || name === \"init\";\n}\n\nfunction createCliProgram(logger: CliLogger): Command {\n const program = new Command();\n const theme = createCliTheme();\n\n program\n .name(\"arcie\")\n .description(\"arcie — the electronic line, build agents at the speed of light\")\n .version(version())\n .showHelpAfterError()\n .exitOverride()\n .hook(\"preAction\", (_program, actionCommand) => {\n if (shouldPrintCliBootBanner(actionCommand)) {\n logger.log(arcieCliBanner());\n }\n })\n .configureOutput({\n writeErr: (message) => logger.error(message.trimEnd()),\n writeOut: (message) => logger.log(message.trimEnd()),\n });\n\n const channels = program\n .command(\"channels\")\n .description(\"Manage channels for the current agent (web UI, HTTP, etc.).\");\n\n channels\n .command(\"add <kind>\")\n .description(\"Scaffold a channel (currently supports: web).\")\n .option(\"--agent-dir <path>\", \"Path to agent directory\", \".\")\n .action(async (kind: string, options: { agentDir: string }) => {\n if (kind !== \"web\") {\n logger.error(`unknown channel kind: ${kind}. supported: web`);\n process.exit(1);\n }\n const agentDir = resolvePath(process.cwd(), options.agentDir);\n const { scaffoldWebChat } = await import(\"./scaffold-web-chat\");\n try {\n const result = scaffoldWebChat(agentDir);\n if (result.alreadyExisted) {\n logger.error(`channels/web already exists at ${result.targetPath}`);\n process.exit(1);\n }\n logger.log(\n renderCliTaggedLine(theme, {\n message: `scaffolded ${result.targetPath}`,\n tag: \"channels\",\n tone: \"success\",\n }),\n );\n logger.log(\n renderCliTaggedLine(theme, {\n message: \"cd into it, then: npm install && cp .env.local.example .env.local && npm run dev\",\n tag: \"next\",\n tone: \"info\",\n }),\n );\n } catch (err) {\n logger.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n });\n\n program\n .command(\"init [target]\")\n .description(\"Create a new arcie agent, or add one to an existing project directory.\")\n .option(\"--template <name>\", \"Template to use (default, agent-only)\", \"default\")\n .option(\"-y, --yes\", \"Accepted for compatibility; has no effect\")\n .action(async (target: string | undefined, options: { template: string; yes?: boolean }) => {\n if (options.yes) logger.error(\"warning: --yes has no effect for arcie init.\");\n const { initCommand } = await import(\"./init\");\n await initCommand(target, { template: options.template });\n });\n\n program\n .command(\"build\")\n .description(\"Compile the agent for production.\")\n .option(\"--agent-dir <path>\", \"Path to agent directory\", \"agent\")\n .option(\"--out-dir <path>\", \"Output directory\", \".arcie\")\n .action(async (options: { agentDir: string; outDir: string }) => {\n const { buildCommand } = await import(\"./build\");\n await buildCommand(options);\n logger.log(\n renderCliTaggedLine(theme, {\n message: `output at ${options.outDir}`,\n tag: \"build\",\n tone: \"success\",\n }),\n );\n });\n\n program\n .command(\"dev\")\n .description(\"Start the arcie development server or attach an interactive UI.\")\n .option(\"-p, --port <port>\", \"Port to listen on\", parsePortOption, 3000)\n .option(\"--host <host>\", \"Host interface to bind\")\n .option(\"--agent-dir <path>\", \"Path to agent directory\", \"agent\")\n .option(\"--input <text>\", \"Pre-fill the prompt input, or start onboarding with /model\")\n .option(\"--no-ui\", \"Start the server without an interactive UI\")\n .option(\"--name <name>\", \"Title shown in the terminal UI (defaults to the app folder name)\")\n .option(\n \"--tools <mode>\",\n \"How tool calls render: full | collapsed | auto-collapsed | hidden\",\n parseDisplayMode,\n )\n .option(\n \"--reasoning <mode>\",\n \"How reasoning renders: full | collapsed | auto-collapsed | hidden\",\n parseDisplayMode,\n )\n .option(\n \"--subagents <mode>\",\n \"How subagent sections render: full | collapsed | auto-collapsed | hidden\",\n parseDisplayMode,\n )\n .option(\n \"--assistant-response-stats <mode>\",\n \"Assistant header statistic: tokens | tokensPerSecond\",\n parseStatsMode,\n )\n .option(\n \"--context-size <tokens>\",\n \"Model context window size, shown as a usage percentage\",\n parseContextSizeOption,\n )\n .option(\"--logs <mode>\", \"Which logs to show: all | stderr | none\", parseLogsMode)\n .action(async (options) => {\n const { devCommand } = await import(\"./dev\");\n await devCommand({\n port: String(options.port ?? 3000),\n agentDir: options.agentDir,\n input: options.ui === false ? false : Boolean(options.input) || options.ui !== false,\n });\n });\n\n return program;\n}\n\nconst KNOWN_COMMANDS = new Set([\"channels\", \"init\", \"build\", \"dev\", \"help\"]);\n\nfunction resolveArgv(argv: readonly string[]): string[] {\n if (argv.length === 0) return [\"dev\"];\n const first = argv[0]!;\n if (first === \"-h\" || first === \"--help\" || first === \"-V\" || first === \"--version\") {\n return [...argv];\n }\n if (first.startsWith(\"-\")) return [...argv];\n if (KNOWN_COMMANDS.has(first)) return [...argv];\n // Bare positional like `arcie my-agent` — treat as `arcie init my-agent`.\n return [\"init\", ...argv];\n}\n\nexport async function runCli(\n argv: string[] = process.argv.slice(2),\n logger: CliLogger = console,\n): Promise<void> {\n const program = createCliProgram(logger);\n const input = resolveArgv(argv);\n\n try {\n await program.parseAsync(input, { from: \"user\" });\n } catch (error) {\n if (error instanceof CommanderError) {\n if (error.exitCode === 0) return;\n throw new Error(error.message);\n }\n throw error;\n }\n}\n\nrunCli().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(message);\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;;;AACA,SAAS,WAAW,mBAAmB;AACvC,SAAS,SAAS,gBAAgB,4BAA4B;AAS9D,IAAM,gBAAgB,oBAAI,IAAI,CAAC,QAAQ,aAAa,kBAAkB,QAAQ,CAAC;AAC/E,IAAM,cAAc,oBAAI,IAAI,CAAC,UAAU,iBAAiB,CAAC;AACzD,IAAM,YAAY,oBAAI,IAAI,CAAC,OAAO,UAAU,MAAM,CAAC;AAMnD,SAAS,gBAAgB,OAAuB;AAC9C,MAAI,CAAC,UAAU,KAAK,KAAK,GAAG;AAC1B,UAAM,IAAI,qBAAqB,sCAAsC,KAAK,IAAI;AAAA,EAChF;AACA,QAAM,OAAO,OAAO,KAAK;AACzB,MAAI,OAAO,KAAK,OAAO,OAAQ;AAC7B,UAAM,IAAI,qBAAqB,kDAAkD,KAAK,IAAI;AAAA,EAC5F;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,OAAwC;AAChE,MAAI,CAAC,cAAc,IAAI,KAAK,GAAG;AAC7B,UAAM,IAAI;AAAA,MACR,mBAAmB,CAAC,GAAG,aAAa,EAAE,KAAK,IAAI,CAAC,eAAe,KAAK;AAAA,IACtE;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,eAAe,OAA2C;AACjE,MAAI,CAAC,YAAY,IAAI,KAAK,GAAG;AAC3B,UAAM,IAAI;AAAA,MACR,mBAAmB,CAAC,GAAG,WAAW,EAAE,KAAK,IAAI,CAAC,eAAe,KAAK;AAAA,IACpE;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,cAAc,OAA+B;AACpD,MAAI,CAAC,UAAU,IAAI,KAAK,GAAG;AACzB,UAAM,IAAI;AAAA,MACR,mBAAmB,CAAC,GAAG,SAAS,EAAE,KAAK,IAAI,CAAC,eAAe,KAAK;AAAA,IAClE;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,uBAAuB,OAAuB;AACrD,QAAM,OAAO,OAAO,KAAK;AACzB,MAAI,CAAC,OAAO,SAAS,IAAI,KAAK,QAAQ,GAAG;AACvC,UAAM,IAAI,qBAAqB,yCAAyC,KAAK,IAAI;AAAA,EACnF;AACA,SAAO;AACT;AAEA,SAAS,yBAAyB,eAAiC;AACjE,QAAM,OAAO,cAAc,KAAK;AAChC,SAAO,SAAS,UAAU,SAAS,SAAS,SAAS;AACvD;AAEA,SAAS,iBAAiB,QAA4B;AACpD,QAAM,UAAU,IAAI,QAAQ;AAC5B,QAAM,QAAQ,eAAe;AAE7B,UACG,KAAK,OAAO,EACZ,YAAY,sEAAiE,EAC7E,QAAQ,QAAQ,CAAC,EACjB,mBAAmB,EACnB,aAAa,EACb,KAAK,aAAa,CAAC,UAAU,kBAAkB;AAC9C,QAAI,yBAAyB,aAAa,GAAG;AAC3C,aAAO,IAAI,eAAe,CAAC;AAAA,IAC7B;AAAA,EACF,CAAC,EACA,gBAAgB;AAAA,IACf,UAAU,CAAC,YAAY,OAAO,MAAM,QAAQ,QAAQ,CAAC;AAAA,IACrD,UAAU,CAAC,YAAY,OAAO,IAAI,QAAQ,QAAQ,CAAC;AAAA,EACrD,CAAC;AAEH,QAAM,WAAW,QACd,QAAQ,UAAU,EAClB,YAAY,6DAA6D;AAE5E,WACG,QAAQ,YAAY,EACpB,YAAY,+CAA+C,EAC3D,OAAO,sBAAsB,2BAA2B,GAAG,EAC3D,OAAO,OAAO,MAAc,YAAkC;AAC7D,QAAI,SAAS,OAAO;AAClB,aAAO,MAAM,yBAAyB,IAAI,kBAAkB;AAC5D,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,UAAM,WAAW,YAAY,QAAQ,IAAI,GAAG,QAAQ,QAAQ;AAC5D,UAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,kCAAqB;AAC9D,QAAI;AACF,YAAM,SAAS,gBAAgB,QAAQ;AACvC,UAAI,OAAO,gBAAgB;AACzB,eAAO,MAAM,kCAAkC,OAAO,UAAU,EAAE;AAClE,gBAAQ,KAAK,CAAC;AAAA,MAChB;AACA,aAAO;AAAA,QACL,oBAAoB,OAAO;AAAA,UACzB,SAAS,cAAc,OAAO,UAAU;AAAA,UACxC,KAAK;AAAA,UACL,MAAM;AAAA,QACR,CAAC;AAAA,MACH;AACA,aAAO;AAAA,QACL,oBAAoB,OAAO;AAAA,UACzB,SAAS;AAAA,UACT,KAAK;AAAA,UACL,MAAM;AAAA,QACR,CAAC;AAAA,MACH;AAAA,IACF,SAAS,KAAK;AACZ,aAAO,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAC7D,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF,CAAC;AAEH,UACG,QAAQ,eAAe,EACvB,YAAY,wEAAwE,EACpF,OAAO,qBAAqB,yCAAyC,SAAS,EAC9E,OAAO,aAAa,2CAA2C,EAC/D,OAAO,OAAO,QAA4B,YAAiD;AAC1F,QAAI,QAAQ,IAAK,QAAO,MAAM,8CAA8C;AAC5E,UAAM,EAAE,YAAY,IAAI,MAAM,OAAO,qBAAQ;AAC7C,UAAM,YAAY,QAAQ,EAAE,UAAU,QAAQ,SAAS,CAAC;AAAA,EAC1D,CAAC;AAEH,UACG,QAAQ,OAAO,EACf,YAAY,mCAAmC,EAC/C,OAAO,sBAAsB,2BAA2B,OAAO,EAC/D,OAAO,oBAAoB,oBAAoB,QAAQ,EACvD,OAAO,OAAO,YAAkD;AAC/D,UAAM,EAAE,aAAa,IAAI,MAAM,OAAO,sBAAS;AAC/C,UAAM,aAAa,OAAO;AAC1B,WAAO;AAAA,MACL,oBAAoB,OAAO;AAAA,QACzB,SAAS,aAAa,QAAQ,MAAM;AAAA,QACpC,KAAK;AAAA,QACL,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAEH,UACG,QAAQ,KAAK,EACb,YAAY,iEAAiE,EAC7E,OAAO,qBAAqB,qBAAqB,iBAAiB,GAAI,EACtE,OAAO,iBAAiB,wBAAwB,EAChD,OAAO,sBAAsB,2BAA2B,OAAO,EAC/D,OAAO,kBAAkB,4DAA4D,EACrF,OAAO,WAAW,4CAA4C,EAC9D,OAAO,iBAAiB,kEAAkE,EAC1F;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC,OAAO,iBAAiB,2CAA2C,aAAa,EAChF,OAAO,OAAO,YAAY;AACzB,UAAM,EAAE,WAAW,IAAI,MAAM,OAAO,oBAAO;AAC3C,UAAM,WAAW;AAAA,MACf,MAAM,OAAO,QAAQ,QAAQ,GAAI;AAAA,MACjC,UAAU,QAAQ;AAAA,MAClB,OAAO,QAAQ,OAAO,QAAQ,QAAQ,QAAQ,QAAQ,KAAK,KAAK,QAAQ,OAAO;AAAA,IACjF,CAAC;AAAA,EACH,CAAC;AAEH,SAAO;AACT;AAEA,IAAM,iBAAiB,oBAAI,IAAI,CAAC,YAAY,QAAQ,SAAS,OAAO,MAAM,CAAC;AAE3E,SAAS,YAAY,MAAmC;AACtD,MAAI,KAAK,WAAW,EAAG,QAAO,CAAC,KAAK;AACpC,QAAM,QAAQ,KAAK,CAAC;AACpB,MAAI,UAAU,QAAQ,UAAU,YAAY,UAAU,QAAQ,UAAU,aAAa;AACnF,WAAO,CAAC,GAAG,IAAI;AAAA,EACjB;AACA,MAAI,MAAM,WAAW,GAAG,EAAG,QAAO,CAAC,GAAG,IAAI;AAC1C,MAAI,eAAe,IAAI,KAAK,EAAG,QAAO,CAAC,GAAG,IAAI;AAE9C,SAAO,CAAC,QAAQ,GAAG,IAAI;AACzB;AAEA,eAAsB,OACpB,OAAiB,QAAQ,KAAK,MAAM,CAAC,GACrC,SAAoB,SACL;AACf,QAAM,UAAU,iBAAiB,MAAM;AACvC,QAAM,QAAQ,YAAY,IAAI;AAE9B,MAAI;AACF,UAAM,QAAQ,WAAW,OAAO,EAAE,MAAM,OAAO,CAAC;AAAA,EAClD,SAAS,OAAO;AACd,QAAI,iBAAiB,gBAAgB;AACnC,UAAI,MAAM,aAAa,EAAG;AAC1B,YAAM,IAAI,MAAM,MAAM,OAAO;AAAA,IAC/B;AACA,UAAM;AAAA,EACR;AACF;AAEA,OAAO,EAAE,MAAM,CAAC,UAAU;AACxB,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,UAAQ,MAAM,OAAO;AACrB,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "arcie",
3
3
  "type": "module",
4
- "version": "0.1.5",
4
+ "version": "0.1.6",
5
5
  "description": "The electronic line — build agents at the speed of light.",
6
6
  "bin": {
7
7
  "arcie": "dist/cli/index.js"
@@ -49,7 +49,17 @@
49
49
  * {
50
50
  @apply border-border;
51
51
  }
52
+ html,
52
53
  body {
53
54
  @apply bg-background text-foreground antialiased;
55
+ min-height: 100%;
56
+ }
57
+ /* Hide Next.js dev-mode injected UI (SegmentExplorer, portals, toasts). */
58
+ nextjs-portal,
59
+ [data-nextjs-toast-wrapper],
60
+ [data-nextjs-dialog-overlay],
61
+ [data-nextjs-scroll-focus-boundary],
62
+ [data-nextjs-devtools-panel] {
63
+ display: none !important;
54
64
  }
55
65
  }
@@ -8,7 +8,7 @@ export const metadata: Metadata = {
8
8
 
9
9
  export default function RootLayout({ children }: { children: React.ReactNode }) {
10
10
  return (
11
- <html lang="en" className="h-full">
11
+ <html lang="en" className="h-full dark" suppressHydrationWarning>
12
12
  <body className="h-full">{children}</body>
13
13
  </html>
14
14
  );
@@ -2,7 +2,7 @@ import { Chat } from "@/components/chat";
2
2
 
3
3
  export default function Page() {
4
4
  return (
5
- <main className="flex h-full flex-col">
5
+ <main className="h-full">
6
6
  <Chat />
7
7
  </main>
8
8
  );
@@ -1,12 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import { Zap } from "lucide-react";
5
4
  import { InputBar } from "@/components/input-bar";
6
5
  import { Message } from "@/components/message";
7
6
  import { readArcieStream } from "@/lib/stream";
8
7
  import type { UiMessage, UiToolCall } from "@/lib/types";
9
- import { cn } from "@/lib/utils";
10
8
 
11
9
  function newId(prefix: string): string {
12
10
  return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -16,10 +14,12 @@ export function Chat() {
16
14
  const [messages, setMessages] = React.useState<UiMessage[]>([]);
17
15
  const [streaming, setStreaming] = React.useState(false);
18
16
  const abortRef = React.useRef<AbortController | undefined>(undefined);
19
- const bottomRef = React.useRef<HTMLDivElement>(null);
17
+ const containerRef = React.useRef<HTMLDivElement>(null);
20
18
 
21
19
  React.useEffect(() => {
22
- bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
20
+ const el = containerRef.current;
21
+ if (el === null) return;
22
+ el.scrollTop = el.scrollHeight;
23
23
  }, [messages]);
24
24
 
25
25
  const stop = () => {
@@ -27,170 +27,193 @@ export function Chat() {
27
27
  setStreaming(false);
28
28
  };
29
29
 
30
- const send = async (text: string) => {
31
- const userMessage: UiMessage = { id: newId("u"), role: "user", content: text };
32
- const assistantId = newId("a");
33
- const assistantMessage: UiMessage = {
34
- id: assistantId,
35
- role: "assistant",
36
- content: "",
37
- streaming: true,
38
- toolCalls: [],
39
- };
40
- setMessages((prev) => [...prev, userMessage, assistantMessage]);
41
- setStreaming(true);
42
-
43
- const controller = new AbortController();
44
- abortRef.current = controller;
45
-
46
- const updateAssistant = (patch: (prev: UiMessage) => UiMessage) => {
47
- setMessages((prev) =>
48
- prev.map((message) => (message.id === assistantId ? patch(message) : message)),
49
- );
50
- };
51
-
52
- try {
53
- const response = await fetch("/api/chat", {
54
- method: "POST",
55
- headers: { "Content-Type": "application/json" },
56
- body: JSON.stringify({ message: text }),
57
- signal: controller.signal,
58
- });
59
- if (!response.ok) {
60
- const errorBody = await response.text();
61
- updateAssistant((m) => ({
62
- ...m,
63
- content: errorBody || `Server error (${response.status})`,
64
- streaming: false,
65
- errored: true,
66
- }));
67
- return;
68
- }
30
+ const clearAll = () => {
31
+ if (streaming) stop();
32
+ setMessages([]);
33
+ };
69
34
 
70
- const toolIndex = new Map<string, number>();
35
+ const copy = (text: string) => {
36
+ void navigator.clipboard.writeText(text);
37
+ };
71
38
 
72
- for await (const event of readArcieStream(response)) {
73
- switch (event.type) {
74
- case "message.appended": {
75
- const delta = (event.data as { delta?: string }).delta ?? "";
76
- updateAssistant((m) => ({ ...m, content: `${m.content}${delta}` }));
77
- break;
78
- }
79
- case "message.completed": {
80
- const text = (event.data as { text?: string | null }).text;
81
- updateAssistant((m) => ({
82
- ...m,
83
- content: typeof text === "string" && text.length > 0 ? text : m.content,
84
- }));
85
- break;
86
- }
87
- case "reasoning.appended": {
88
- const delta = (event.data as { delta?: string }).delta ?? "";
89
- updateAssistant((m) => ({ ...m, reasoning: `${m.reasoning ?? ""}${delta}` }));
90
- break;
91
- }
92
- case "tool.started": {
93
- const data = event.data as { name: string; callId: string; input: unknown };
94
- const call: UiToolCall = {
95
- callId: data.callId,
96
- name: data.name,
97
- input: data.input,
98
- status: "running",
99
- };
100
- updateAssistant((m) => {
101
- const toolCalls = m.toolCalls ?? [];
102
- toolIndex.set(data.callId, toolCalls.length);
103
- return { ...m, toolCalls: [...toolCalls, call] };
104
- });
105
- break;
106
- }
107
- case "tool.completed": {
108
- const data = event.data as {
109
- callId: string;
110
- output: unknown;
111
- status: string;
112
- error?: { code: string; message: string };
113
- };
114
- updateAssistant((m) => {
115
- const toolCalls = [...(m.toolCalls ?? [])];
116
- const idx = toolIndex.get(data.callId);
117
- if (idx === undefined || toolCalls[idx] === undefined) return m;
118
- const previous = toolCalls[idx];
119
- const isApproval =
120
- data.status === "pending" && data.error?.code === "needs_approval";
121
- toolCalls[idx] = {
122
- ...previous,
123
- status: isApproval
124
- ? "approval"
125
- : data.status === "completed"
126
- ? "done"
127
- : "error",
128
- output: data.output,
129
- errorMessage: data.error?.message,
39
+ const send = React.useCallback(
40
+ async (text: string, historyOverride?: UiMessage[]) => {
41
+ const base = historyOverride ?? messages;
42
+ const userMessage: UiMessage = { id: newId("u"), role: "user", content: text };
43
+ const assistantId = newId("a");
44
+ const assistantMessage: UiMessage = {
45
+ id: assistantId,
46
+ role: "assistant",
47
+ content: "",
48
+ streaming: true,
49
+ toolCalls: [],
50
+ };
51
+ const isRegeneration = historyOverride !== undefined;
52
+ setMessages((prev) => (isRegeneration ? [...base, assistantMessage] : [...prev, userMessage, assistantMessage]));
53
+ setStreaming(true);
54
+
55
+ const controller = new AbortController();
56
+ abortRef.current = controller;
57
+ const startedAt = Date.now();
58
+
59
+ const patchAssistant = (patch: (prev: UiMessage) => UiMessage) => {
60
+ setMessages((prev) =>
61
+ prev.map((message) => (message.id === assistantId ? patch(message) : message)),
62
+ );
63
+ };
64
+
65
+ try {
66
+ const response = await fetch("/api/chat", {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({ message: text }),
70
+ signal: controller.signal,
71
+ });
72
+ if (!response.ok) {
73
+ const body = await response.text();
74
+ patchAssistant((m) => ({
75
+ ...m,
76
+ content: body || `Server error (${response.status})`,
77
+ streaming: false,
78
+ errored: true,
79
+ }));
80
+ return;
81
+ }
82
+
83
+ const toolIndex = new Map<string, number>();
84
+
85
+ for await (const event of readArcieStream(response)) {
86
+ switch (event.type) {
87
+ case "message.appended": {
88
+ const delta = (event.data as { delta?: string }).delta ?? "";
89
+ patchAssistant((m) => ({ ...m, content: `${m.content}${delta}` }));
90
+ break;
91
+ }
92
+ case "message.completed": {
93
+ const finished = (event.data as { text?: string | null }).text;
94
+ patchAssistant((m) => ({
95
+ ...m,
96
+ content: typeof finished === "string" && finished.length > 0 ? finished : m.content,
97
+ }));
98
+ break;
99
+ }
100
+ case "reasoning.appended": {
101
+ const delta = (event.data as { delta?: string }).delta ?? "";
102
+ patchAssistant((m) => ({ ...m, reasoning: `${m.reasoning ?? ""}${delta}` }));
103
+ break;
104
+ }
105
+ case "tool.started": {
106
+ const data = event.data as { name: string; callId: string; input: unknown };
107
+ const call: UiToolCall = {
108
+ callId: data.callId,
109
+ name: data.name,
110
+ input: data.input,
111
+ status: "running",
130
112
  };
131
- return { ...m, toolCalls };
132
- });
133
- break;
134
- }
135
- case "step.failed":
136
- case "turn.failed":
137
- case "session.failed": {
138
- const data = event.data as { code?: string; message?: string };
139
- updateAssistant((m) => ({
140
- ...m,
141
- content: data.message ?? "Something went wrong.",
142
- streaming: false,
143
- errored: true,
144
- }));
145
- break;
113
+ patchAssistant((m) => {
114
+ const toolCalls = m.toolCalls ?? [];
115
+ toolIndex.set(data.callId, toolCalls.length);
116
+ return { ...m, toolCalls: [...toolCalls, call] };
117
+ });
118
+ break;
119
+ }
120
+ case "tool.completed": {
121
+ const data = event.data as {
122
+ callId: string;
123
+ output: unknown;
124
+ status: string;
125
+ error?: { code: string; message: string };
126
+ };
127
+ patchAssistant((m) => {
128
+ const toolCalls = [...(m.toolCalls ?? [])];
129
+ const idx = toolIndex.get(data.callId);
130
+ if (idx === undefined || toolCalls[idx] === undefined) return m;
131
+ const previous = toolCalls[idx];
132
+ const isApproval =
133
+ data.status === "pending" && data.error?.code === "needs_approval";
134
+ toolCalls[idx] = {
135
+ ...previous,
136
+ status: isApproval
137
+ ? "approval"
138
+ : data.status === "completed"
139
+ ? "done"
140
+ : "error",
141
+ output: data.output,
142
+ errorMessage: data.error?.message,
143
+ };
144
+ return { ...m, toolCalls };
145
+ });
146
+ break;
147
+ }
148
+ case "step.failed":
149
+ case "turn.failed":
150
+ case "session.failed": {
151
+ const data = event.data as { code?: string; message?: string };
152
+ patchAssistant((m) => ({
153
+ ...m,
154
+ content: data.message ?? "Something went wrong.",
155
+ streaming: false,
156
+ errored: true,
157
+ }));
158
+ break;
159
+ }
146
160
  }
147
161
  }
162
+ } catch (error) {
163
+ if (!controller.signal.aborted) {
164
+ patchAssistant((m) => ({
165
+ ...m,
166
+ content: error instanceof Error ? error.message : String(error),
167
+ streaming: false,
168
+ errored: true,
169
+ }));
170
+ }
171
+ } finally {
172
+ const latencyMs = Date.now() - startedAt;
173
+ patchAssistant((m) => ({ ...m, streaming: false, latencyMs }));
174
+ setStreaming(false);
175
+ abortRef.current = undefined;
148
176
  }
149
- } catch (error) {
150
- if (controller.signal.aborted) {
151
- updateAssistant((m) => ({ ...m, streaming: false }));
152
- } else {
153
- updateAssistant((m) => ({
154
- ...m,
155
- content: error instanceof Error ? error.message : String(error),
156
- streaming: false,
157
- errored: true,
158
- }));
159
- }
160
- } finally {
161
- updateAssistant((m) => ({ ...m, streaming: false }));
162
- setStreaming(false);
163
- abortRef.current = undefined;
164
- }
177
+ },
178
+ [messages],
179
+ );
180
+
181
+ const regenerate = () => {
182
+ const lastUserIndex = messages.map((m) => m.role).lastIndexOf("user");
183
+ if (lastUserIndex === -1) return;
184
+ const historyBefore = messages.slice(0, lastUserIndex);
185
+ const lastUser = messages[lastUserIndex]!;
186
+ setMessages([...historyBefore, lastUser]);
187
+ void send(lastUser.content, [...historyBefore, lastUser]);
165
188
  };
166
189
 
167
190
  return (
168
- <div className="flex h-full flex-col">
169
- <header className="flex items-center gap-2 border-b border-border px-4 py-3">
170
- <Zap className="h-5 w-5 text-amber-500" />
171
- <span className="font-semibold">arcie</span>
172
- <span className="text-xs text-muted-foreground">web chat</span>
173
- </header>
174
- <div className="flex-1 overflow-y-auto">
175
- <div className="mx-auto flex max-w-3xl flex-col gap-3 px-4 py-6">
176
- {messages.length === 0 && <EmptyState />}
177
- {messages.map((message) => (
178
- <Message key={message.id} message={message} />
179
- ))}
180
- <div ref={bottomRef} />
191
+ <div className="fixed inset-0 flex flex-col overflow-hidden bg-background text-foreground">
192
+ <div ref={containerRef} className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
193
+ <div className="mx-auto flex min-h-full w-full max-w-3xl flex-col justify-start gap-6 pt-8 pb-4">
194
+ {messages.map((message, index) => {
195
+ const isLast =
196
+ message.role === "assistant" && index === messages.length - 1 && !streaming;
197
+ return (
198
+ <Message
199
+ key={message.id}
200
+ message={message}
201
+ isLast={isLast}
202
+ onCopy={copy}
203
+ onRegenerate={regenerate}
204
+ />
205
+ );
206
+ })}
181
207
  </div>
182
208
  </div>
183
- <InputBar onSend={send} onStop={stop} streaming={streaming} />
184
- </div>
185
- );
186
- }
187
209
 
188
- function EmptyState() {
189
- return (
190
- <div className={cn("mt-24 flex flex-col items-center gap-2 text-center text-muted-foreground")}>
191
- <Zap className="h-8 w-8 text-amber-500" />
192
- <div className="text-lg font-medium text-foreground">Start a conversation</div>
193
- <div className="text-sm">Ask your arcie agent anything.</div>
210
+ <InputBar
211
+ onSend={(text) => void send(text)}
212
+ onStop={stop}
213
+ onClear={clearAll}
214
+ streaming={streaming}
215
+ hasMessages={messages.length > 0}
216
+ />
194
217
  </div>
195
218
  );
196
219
  }
@@ -1,26 +1,39 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import { Send, StopCircle } from "lucide-react";
5
- import { Button } from "@/components/ui/button";
4
+ import { ArrowUp, Paperclip, StopCircle, Trash2, X } from "lucide-react";
6
5
  import { cn } from "@/lib/utils";
7
6
 
8
7
  interface InputBarProps {
9
- onSend(message: string): void;
8
+ onSend(message: string, files?: File[]): void;
10
9
  onStop(): void;
10
+ onClear?(): void;
11
11
  streaming: boolean;
12
12
  disabled?: boolean;
13
+ hasMessages?: boolean;
13
14
  }
14
15
 
15
- export function InputBar({ onSend, onStop, streaming, disabled }: InputBarProps) {
16
+ export function InputBar({
17
+ onSend,
18
+ onStop,
19
+ onClear,
20
+ streaming,
21
+ disabled,
22
+ hasMessages,
23
+ }: InputBarProps) {
16
24
  const [value, setValue] = React.useState("");
25
+ const [files, setFiles] = React.useState<File[]>([]);
17
26
  const textareaRef = React.useRef<HTMLTextAreaElement>(null);
27
+ const fileInputRef = React.useRef<HTMLInputElement>(null);
18
28
 
19
29
  const submit = () => {
20
30
  const trimmed = value.trim();
21
- if (trimmed.length === 0 || streaming || disabled) return;
22
- onSend(trimmed);
31
+ if ((trimmed.length === 0 && files.length === 0) || streaming || disabled) return;
32
+ onSend(trimmed, files.length > 0 ? files : undefined);
23
33
  setValue("");
34
+ setFiles([]);
35
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
36
+ if (fileInputRef.current) fileInputRef.current.value = "";
24
37
  };
25
38
 
26
39
  const onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -30,44 +43,138 @@ export function InputBar({ onSend, onStop, streaming, disabled }: InputBarProps)
30
43
  }
31
44
  };
32
45
 
33
- React.useEffect(() => {
34
- const el = textareaRef.current;
35
- if (el === null) return;
46
+ const onChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
47
+ setValue(event.target.value);
48
+ const el = event.target;
36
49
  el.style.height = "auto";
37
- el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
38
- }, [value]);
50
+ el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
51
+ };
52
+
53
+ const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
54
+ const next = event.target.files;
55
+ if (next === null) return;
56
+ setFiles((prev) => [...prev, ...Array.from(next)]);
57
+ };
58
+
59
+ const removeFile = (index: number) => {
60
+ setFiles((prev) => prev.filter((_, i) => i !== index));
61
+ };
62
+
63
+ const openFilePicker = () => {
64
+ fileInputRef.current?.click();
65
+ };
39
66
 
40
67
  return (
41
- <div className="border-t border-border bg-background px-4 py-3">
42
- <div className="mx-auto flex max-w-3xl items-end gap-2">
43
- <textarea
44
- ref={textareaRef}
45
- value={value}
46
- onChange={(event) => setValue(event.target.value)}
47
- onKeyDown={onKeyDown}
48
- rows={1}
49
- placeholder={streaming ? "…" : "Send a message"}
50
- disabled={disabled}
68
+ <div className="shrink-0 bg-transparent px-4 pt-4 pb-6">
69
+ <div className="mx-auto w-full max-w-3xl">
70
+ <div
51
71
  className={cn(
52
- "flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground",
53
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
54
- "disabled:cursor-not-allowed disabled:opacity-50",
72
+ "relative flex flex-col rounded-2xl border border-border/40 bg-muted/10 backdrop-blur-md p-3 transition-all",
73
+ "hover:border-border/60 hover:bg-muted/20",
74
+ "focus-within:border-primary/45 focus-within:ring-1 focus-within:ring-primary/20",
75
+ )}
76
+ >
77
+ {files.length > 0 && (
78
+ <div className="flex flex-wrap gap-1.5 mb-2">
79
+ {files.map((file, index) => (
80
+ <div
81
+ key={`${file.name}-${index}`}
82
+ className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-card/60 text-[11px] font-medium text-foreground border border-border/30"
83
+ >
84
+ <Paperclip className="h-3 w-3 text-muted-foreground/70" />
85
+ <span className="max-w-[160px] truncate">{file.name}</span>
86
+ <button
87
+ type="button"
88
+ onClick={() => removeFile(index)}
89
+ className="text-muted-foreground/50 hover:text-foreground transition-colors"
90
+ title="Remove attachment"
91
+ >
92
+ <X className="h-3 w-3" />
93
+ </button>
94
+ </div>
95
+ ))}
96
+ </div>
55
97
  )}
56
- />
57
- {streaming ? (
58
- <Button size="icon" variant="outline" onClick={onStop} aria-label="Stop">
59
- <StopCircle className="h-4 w-4" />
60
- </Button>
61
- ) : (
62
- <Button
63
- size="icon"
64
- onClick={submit}
65
- disabled={value.trim().length === 0 || disabled}
66
- aria-label="Send"
67
- >
68
- <Send className="h-4 w-4" />
69
- </Button>
70
- )}
98
+
99
+ <textarea
100
+ ref={textareaRef}
101
+ value={value}
102
+ onChange={onChange}
103
+ onKeyDown={onKeyDown}
104
+ rows={2}
105
+ placeholder={streaming ? "" : "Ask a question..."}
106
+ disabled={disabled}
107
+ className={cn(
108
+ "max-h-40 min-h-[48px] w-full resize-none bg-transparent py-1.5 text-xs",
109
+ "placeholder:text-muted-foreground/50 focus:outline-none leading-relaxed",
110
+ "disabled:cursor-not-allowed disabled:opacity-50",
111
+ )}
112
+ />
113
+
114
+ <div className="flex items-center justify-between pt-2.5 mt-2 select-none">
115
+ <div className="flex items-center gap-1.5">
116
+ <input
117
+ ref={fileInputRef}
118
+ type="file"
119
+ multiple
120
+ onChange={onFileChange}
121
+ className="hidden"
122
+ />
123
+ <button
124
+ type="button"
125
+ onClick={openFilePicker}
126
+ disabled={streaming}
127
+ className={cn(
128
+ "h-8 w-8 flex items-center justify-center rounded-full text-muted-foreground/60 transition-colors",
129
+ "hover:bg-muted/30 hover:text-foreground",
130
+ "disabled:cursor-not-allowed disabled:opacity-40",
131
+ )}
132
+ aria-label="Attach files"
133
+ title="Attach files"
134
+ >
135
+ <Paperclip className="h-4 w-4" />
136
+ </button>
137
+ </div>
138
+
139
+ <div className="flex items-center gap-2 shrink-0">
140
+ {hasMessages && onClear && (
141
+ <button
142
+ type="button"
143
+ onClick={onClear}
144
+ className="h-8 w-8 flex items-center justify-center rounded-full text-muted-foreground/60 hover:bg-muted/30 hover:text-foreground transition-colors"
145
+ aria-label="New chat"
146
+ title="New chat"
147
+ >
148
+ <Trash2 className="h-4 w-4" />
149
+ </button>
150
+ )}
151
+ {streaming ? (
152
+ <button
153
+ type="button"
154
+ onClick={onStop}
155
+ className="h-8 w-8 flex items-center justify-center rounded-full bg-foreground text-background hover:bg-foreground/90 transition-colors"
156
+ aria-label="Stop generation"
157
+ >
158
+ <StopCircle className="h-3.5 w-3.5 fill-current" />
159
+ </button>
160
+ ) : (
161
+ <button
162
+ type="button"
163
+ onClick={submit}
164
+ disabled={(value.trim().length === 0 && files.length === 0) || disabled}
165
+ className={cn(
166
+ "h-8 w-8 flex items-center justify-center rounded-full bg-foreground text-background transition-colors",
167
+ "hover:bg-foreground/90",
168
+ "disabled:cursor-not-allowed disabled:opacity-40",
169
+ )}
170
+ aria-label="Send message"
171
+ >
172
+ <ArrowUp className="h-3.5 w-3.5" />
173
+ </button>
174
+ )}
175
+ </div>
176
+ </div>
177
+ </div>
71
178
  </div>
72
179
  </div>
73
180
  );
@@ -3,58 +3,118 @@
3
3
  import * as React from "react";
4
4
  import ReactMarkdown from "react-markdown";
5
5
  import remarkGfm from "remark-gfm";
6
- import { AlertTriangle } from "lucide-react";
6
+ import { AlertTriangle, Copy, RotateCcw } from "lucide-react";
7
7
  import { cn } from "@/lib/utils";
8
8
  import type { UiMessage } from "@/lib/types";
9
9
  import { ToolCall } from "@/components/tool-call";
10
10
 
11
- export function Message({ message }: { message: UiMessage }) {
11
+ interface MessageProps {
12
+ message: UiMessage;
13
+ isLast?: boolean;
14
+ onCopy?(text: string): void;
15
+ onRegenerate?(): void;
16
+ }
17
+
18
+ export function Message({ message, isLast, onCopy, onRegenerate }: MessageProps) {
12
19
  const isUser = message.role === "user";
20
+
21
+ if (isUser) {
22
+ return (
23
+ <div className="flex flex-col px-4 items-end">
24
+ <div className="max-w-[85%] rounded-2xl rounded-tr-sm bg-primary px-3.5 py-2 text-primary-foreground shadow-sm">
25
+ <p className="text-xs font-medium leading-relaxed whitespace-pre-wrap">
26
+ {message.content}
27
+ </p>
28
+ </div>
29
+ </div>
30
+ );
31
+ }
32
+
13
33
  return (
14
- <div className={cn("flex w-full", isUser ? "justify-end" : "justify-start")}>
15
- <div
16
- className={cn(
17
- "max-w-[85%] rounded-lg px-4 py-2.5 text-sm animate-fade-in",
18
- isUser
19
- ? "bg-primary text-primary-foreground"
20
- : "bg-muted text-foreground",
21
- message.errored && "border border-destructive/40",
22
- )}
23
- >
34
+ <div className="flex flex-col px-4 items-start">
35
+ <div className="w-full space-y-2">
24
36
  {message.errored && (
25
37
  <div className="mb-2 flex items-center gap-2 text-xs text-destructive">
26
38
  <AlertTriangle className="h-3 w-3" />
27
39
  <span>Error</span>
28
40
  </div>
29
41
  )}
42
+
30
43
  {message.reasoning && (
31
- <details className="mb-2 text-xs text-muted-foreground">
32
- <summary className="cursor-pointer select-none">thinking</summary>
33
- <div className="mt-1 whitespace-pre-wrap italic">{message.reasoning}</div>
44
+ <details className="text-[11px] text-muted-foreground/70 border border-border/20 rounded-lg px-2.5 py-1.5 bg-muted/10">
45
+ <summary className="cursor-pointer select-none font-medium">thinking</summary>
46
+ <div className="mt-1.5 whitespace-pre-wrap italic leading-relaxed">
47
+ {message.reasoning}
48
+ </div>
34
49
  </details>
35
50
  )}
36
- {message.content.length > 0 &&
37
- (isUser ? (
38
- <div className="whitespace-pre-wrap">{message.content}</div>
39
- ) : (
40
- <div className="prose prose-sm prose-zinc max-w-none dark:prose-invert">
41
- <ReactMarkdown remarkPlugins={[remarkGfm]}>
42
- {message.content}
43
- </ReactMarkdown>
51
+
52
+ {message.streaming && message.content.length === 0 && !message.toolCalls?.length ? (
53
+ <span className="inline-flex items-center gap-[3px]">
54
+ <span
55
+ className="w-1.5 h-1.5 rounded-full bg-muted-foreground/40 animate-bounce"
56
+ style={{ animationDelay: "0ms" }}
57
+ />
58
+ <span
59
+ className="w-1.5 h-1.5 rounded-full bg-muted-foreground/40 animate-bounce"
60
+ style={{ animationDelay: "150ms" }}
61
+ />
62
+ <span
63
+ className="w-1.5 h-1.5 rounded-full bg-muted-foreground/40 animate-bounce"
64
+ style={{ animationDelay: "300ms" }}
65
+ />
66
+ </span>
67
+ ) : (
68
+ message.content.length > 0 && (
69
+ <div
70
+ className={cn(
71
+ "prose prose-sm prose-zinc max-w-none dark:prose-invert",
72
+ "prose-p:my-2 prose-p:leading-relaxed prose-p:text-xs",
73
+ "prose-pre:my-2 prose-pre:text-xs",
74
+ "prose-code:text-xs prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none",
75
+ message.errored && "text-destructive",
76
+ )}
77
+ >
78
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
44
79
  </div>
45
- ))}
80
+ )
81
+ )}
82
+
46
83
  {message.toolCalls && message.toolCalls.length > 0 && (
47
- <div className="mt-2 flex flex-col gap-1.5">
84
+ <div className="flex flex-col gap-1.5">
48
85
  {message.toolCalls.map((call) => (
49
86
  <ToolCall key={call.callId} call={call} />
50
87
  ))}
51
88
  </div>
52
89
  )}
53
- {message.streaming && message.content.length === 0 && !message.toolCalls?.length && (
54
- <div className="flex gap-1">
55
- <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-muted-foreground" />
56
- <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-muted-foreground [animation-delay:150ms]" />
57
- <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-muted-foreground [animation-delay:300ms]" />
90
+
91
+ {!message.streaming && message.content.length > 0 && (
92
+ <div className="flex flex-wrap items-center gap-1.5 pt-1">
93
+ {message.latencyMs !== undefined && (
94
+ <span className="h-5 rounded border border-border/30 bg-muted/5 px-1.5 font-mono text-[9px] text-muted-foreground/70">
95
+ {message.latencyMs}ms
96
+ </span>
97
+ )}
98
+ {onCopy && (
99
+ <button
100
+ type="button"
101
+ onClick={() => onCopy(message.content)}
102
+ className="h-6 w-6 flex items-center justify-center rounded text-muted-foreground/60 hover:text-foreground hover:bg-muted/30 transition-colors"
103
+ title="Copy to clipboard"
104
+ >
105
+ <Copy className="h-3 w-3" />
106
+ </button>
107
+ )}
108
+ {isLast && onRegenerate && (
109
+ <button
110
+ type="button"
111
+ onClick={onRegenerate}
112
+ className="h-6 w-6 flex items-center justify-center rounded text-muted-foreground/60 hover:text-foreground hover:bg-muted/30 transition-colors"
113
+ title="Regenerate response"
114
+ >
115
+ <RotateCcw className="h-3 w-3" />
116
+ </button>
117
+ )}
58
118
  </div>
59
119
  )}
60
120
  </div>
@@ -44,18 +44,18 @@ export function ToolCall({ call }: { call: UiToolCall }) {
44
44
  return (
45
45
  <div
46
46
  className={cn(
47
- "flex flex-col gap-1 rounded-md border border-border bg-muted/40 px-3 py-2 text-sm",
47
+ "flex flex-col gap-1 rounded-lg border border-border/30 bg-muted/10 px-3 py-2 text-xs",
48
48
  )}
49
49
  >
50
50
  <div className="flex items-center gap-2">
51
51
  <StatusGlyph status={call.status} />
52
- <span className="font-mono font-medium">{call.name}</span>
52
+ <span className="font-mono font-medium text-[11px]">{call.name}</span>
53
53
  {args.length > 0 && (
54
- <span className="truncate font-mono text-xs text-muted-foreground">{args}</span>
54
+ <span className="truncate font-mono text-[10px] text-muted-foreground/70">{args}</span>
55
55
  )}
56
56
  </div>
57
57
  {result !== undefined && (
58
- <div className="ml-6 truncate text-xs text-muted-foreground">
58
+ <div className="ml-6 truncate text-[10px] text-muted-foreground/70">
59
59
  → <span className="font-mono">{result}</span>
60
60
  </div>
61
61
  )}
@@ -66,13 +66,13 @@ export function ToolCall({ call }: { call: UiToolCall }) {
66
66
  function StatusGlyph({ status }: { status: UiToolCall["status"] }) {
67
67
  switch (status) {
68
68
  case "running":
69
- return <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />;
69
+ return <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />;
70
70
  case "done":
71
- return <CheckCircle2 className="h-4 w-4 text-emerald-500" />;
71
+ return <CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" />;
72
72
  case "error":
73
- return <XCircle className="h-4 w-4 text-destructive" />;
73
+ return <XCircle className="h-3.5 w-3.5 text-destructive" />;
74
74
  case "approval":
75
- return <HelpCircle className="h-4 w-4 text-amber-500" />;
75
+ return <HelpCircle className="h-3.5 w-3.5 text-amber-500" />;
76
76
  default:
77
77
  return null;
78
78
  }
@@ -8,6 +8,7 @@ export interface UiMessage {
8
8
  reasoning?: string;
9
9
  streaming?: boolean;
10
10
  errored?: boolean;
11
+ latencyMs?: number;
11
12
  }
12
13
 
13
14
  export interface UiToolCall {
@@ -1,4 +1,6 @@
1
1
  /** @type {import('next').NextConfig} */
2
- const nextConfig = {};
2
+ const nextConfig = {
3
+ devIndicators: false,
4
+ };
3
5
 
4
6
  export default nextConfig;
@@ -21,6 +21,7 @@
21
21
  "tailwindcss-animate": "^1.0.7"
22
22
  },
23
23
  "devDependencies": {
24
+ "@tailwindcss/typography": "^0.5.15",
24
25
  "@types/node": "^22.10.0",
25
26
  "@types/react": "^19.0.0",
26
27
  "@types/react-dom": "^19.0.0",
@@ -1,4 +1,6 @@
1
1
  import type { Config } from "tailwindcss";
2
+ import tailwindcssAnimate from "tailwindcss-animate";
3
+ import tailwindcssTypography from "@tailwindcss/typography";
2
4
 
3
5
  const config: Config = {
4
6
  darkMode: ["class"],
@@ -53,7 +55,7 @@ const config: Config = {
53
55
  },
54
56
  },
55
57
  },
56
- plugins: [require("tailwindcss-animate")],
58
+ plugins: [tailwindcssAnimate, tailwindcssTypography],
57
59
  };
58
60
 
59
61
  export default config;