From 94cdcfa6f44892af6d3974ebbc7c059e3bbddab2 Mon Sep 17 00:00:00 2001 From: lmanualm Date: Mon, 7 Sep 2026 00:09:06 +0200 Subject: [PATCH 1/2] Add native Codex and Claude CLI command access --- docs/agent-cli.md | 43 +++++ docs/validation/agent-cli-menu.png | Bin 0 -> 40463 bytes src-tauri/src/pty.rs | 48 ++++- src/chrome/Composer.tsx | 99 ++++++++++- src/chrome/SkillPicker.tsx | 84 +++++++-- src/lib/agentCli.test.ts | 132 ++++++++++++++ src/lib/agentCli.ts | 120 +++++++++++++ src/lib/claudeCliCommands.json | 114 ++++++++++++ src/lib/codexCliCommands.json | 59 +++++++ src/lib/pty.ts | 13 +- src/surfaces/AgentCliView.tsx | 155 +++++++++++++++++ src/surfaces/SessionPane.tsx | 269 +++++++++++++++++------------ src/surfaces/TerminalView.tsx | 45 ++++- tests/agent-cli.html | 11 ++ tests/agent-cli.tsx | 100 +++++++++++ 15 files changed, 1143 insertions(+), 149 deletions(-) create mode 100644 docs/agent-cli.md create mode 100644 docs/validation/agent-cli-menu.png create mode 100644 src/lib/agentCli.test.ts create mode 100644 src/lib/agentCli.ts create mode 100644 src/lib/claudeCliCommands.json create mode 100644 src/lib/codexCliCommands.json create mode 100644 src/surfaces/AgentCliView.tsx create mode 100644 tests/agent-cli.html create mode 100644 tests/agent-cli.tsx diff --git a/docs/agent-cli.md b/docs/agent-cli.md new file mode 100644 index 00000000..dd904505 --- /dev/null +++ b/docs/agent-cli.md @@ -0,0 +1,43 @@ +# Agent CLI commands + +For Codex and Claude Code, type `/` and choose **All**, **Agent commands**, or +**Skills**. This choice is remembered. **Agent commands** accepts any slash +command, including custom and newly released commands absent from completion. + +Use `/cli:status`, `/cli:model`, or `/cli:compact` to explicitly select the +agent's command in any filter. In **All**, existing skills and MonoCode's +`/plan` and `/compact` retain their meaning. In **Skills**, existing skill +submission is unchanged. The **CLI** button opens the native agent directly. + +Native commands open an embedded terminal running the installed CLI. Complete +its startup prompts, then click **Send to CLI** to send the prepared command, +or type directly in the terminal. Type `/` there for the agent's own complete +menu. Its menus, custom commands, plugins, login, permissions, model settings +and future commands are handled by the installed agent. The suggestion catalog +contains documented command names; availability can differ by agent version, +platform, account, and enabled features. + +The native session is **separate**. If this chat has provider history, Codex +uses `fork ` and Claude uses `--resume --fork-session` to copy it. This +prevents two processes from writing the same conversation. Messages and +per-session settings are not synchronized back into the original MonoCode +chat. Project files and the agent account are shared: file edits, sign-out and +global configuration changes still affect other sessions. Use the agent's +`/resume` to reopen native history. +New chats and pending provider switches start a fresh native session. Native +CLI settings apply, including its default model and permission policy. + +The regular composer stays mounted so drafts and attachments survive a visit +to CLI. Opening a CLI is blocked during an active turn or queued follow-ups. +**Close CLI · Back to chat** terminates only that embedded process tree. Closing +the MonoCode pane or app also cleans it up. Closing may interrupt work in that +native session; use `/exit` in the agent when you want it to finish gracefully. + +Validation: `npm run check:web`, `npm run check:rust`. The browser smoke fixture +at `/tests/agent-cli.html` (serve with `npm run dev`) mocks IPC and cannot start +an agent or model turn. It covers the real composer and native-terminal UI. + +Command-name references: + +- [Codex commands](https://learn.chatgpt.com/docs/developer-commands) +- [Claude Code commands](https://code.claude.com/docs/en/commands) diff --git a/docs/validation/agent-cli-menu.png b/docs/validation/agent-cli-menu.png new file mode 100644 index 0000000000000000000000000000000000000000..407cc89401e2be29888b4da057727f3698853b55 GIT binary patch literal 40463 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 literal 0 HcmV?d00001 diff --git a/src-tauri/src/pty.rs b/src-tauri/src/pty.rs index 8686f516..fe36acfe 100644 --- a/src-tauri/src/pty.rs +++ b/src-tauri/src/pty.rs @@ -8,7 +8,7 @@ use std::time::Duration; #[cfg(unix)] use std::time::Instant; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter, Manager, State}; use crate::dirs_home; @@ -50,6 +50,24 @@ pub struct PtyHost { sessions: Mutex>>, } +#[derive(Deserialize)] +pub struct PtyCommand { + program: String, + args: Vec, +} + +impl PtyCommand { + fn validate(&self) -> Result<(), String> { + if self.program.trim().is_empty() + || self.program.contains('\0') + || self.args.iter().any(|arg| arg.contains('\0')) + { + return Err("Invalid terminal command.".into()); + } + Ok(()) + } +} + impl PtyHost { pub fn new() -> Self { Self { @@ -123,7 +141,11 @@ pub fn pty_spawn( cwd: String, cols: u16, rows: u16, + command: Option, ) -> Result<(), String> { + if let Some(command) = &command { + command.validate()?; + } if let Some(prev) = host.remove(&id) { terminate(prev.pid); #[cfg(unix)] @@ -132,17 +154,17 @@ pub fn pty_spawn( #[cfg(unix)] { - spawn_unix(app, host, id, cwd, cols.max(2), rows.max(2)) + spawn_unix(app, host, id, cwd, cols.max(2), rows.max(2), command) } #[cfg(windows)] { - spawn_windows(app, host, id, cwd, cols.max(2), rows.max(2)) + spawn_windows(app, host, id, cwd, cols.max(2), rows.max(2), command) } #[cfg(not(any(unix, windows)))] { - let _ = (app, cwd, cols, rows); + let _ = (app, cwd, cols, rows, command); Err("Terminals are not supported on this platform.".into()) } } @@ -238,6 +260,7 @@ fn spawn_unix( cwd: String, cols: u16, rows: u16, + command: Option, ) -> Result<(), String> { use std::fs::File; use std::os::unix::io::FromRawFd; @@ -245,7 +268,9 @@ fn spawn_unix( use std::process::Command; let workdir = working_dir(&cwd); - let (shell, args) = default_shell(); + let (shell, args) = command + .map(|cmd| (cmd.program, cmd.args)) + .unwrap_or_else(default_shell); let (master, slave) = open_pty(cols, rows)?; let mut cmd = Command::new(&shell); @@ -282,10 +307,12 @@ fn spawn_unix( }); } - let mut child = cmd - .spawn() - .map_err(|e| format!("Failed to start {shell}: {e}"))?; + let result = cmd.spawn(); close_fd(slave); + let mut child = result.map_err(|e| { + close_fd(master); + format!("Failed to start {shell}: {e}") + })?; let pid = child.id(); set_cloexec(master); @@ -367,11 +394,14 @@ fn spawn_windows( cwd: String, cols: u16, rows: u16, + command: Option, ) -> Result<(), String> { use portable_pty::{native_pty_system, CommandBuilder, PtySize}; let workdir = working_dir(&cwd); - let (shell, args) = default_shell(); + let (shell, args) = command + .map(|cmd| (cmd.program, cmd.args)) + .unwrap_or_else(default_shell); let pty_system = native_pty_system(); let pair = pty_system .openpty(PtySize { diff --git a/src/chrome/Composer.tsx b/src/chrome/Composer.tsx index b93cc63a..6e134c67 100644 --- a/src/chrome/Composer.tsx +++ b/src/chrome/Composer.tsx @@ -121,6 +121,15 @@ import { useComposerSkills } from "./useComposerSkills"; import { Popover } from "./Popover"; import { consumePlanCommand, PLAN_COMMAND } from "../lib/plan"; import { COMPACT_COMMAND, isCompactCommand } from "../lib/compact"; +import { + agentCliCommands, + agentCliPrompt, + filterSlashItems, + loadSlashFilter, + saveSlashFilter, + supportsAgentCli, + type SlashFilter, +} from "../lib/agentCli"; type Props = { enabled?: boolean; @@ -154,6 +163,7 @@ type Props = { onCwdChange: (cwd: string) => void; onBranchChange?: () => void; onNewTerminal?: () => void; + onAgentCommand?: (command?: string) => boolean; onModelChange: (harness: HarnessId, model: string) => void; onModelSettingsChange?: (settings: Record) => void; onRuntimeModeChange: (mode: RuntimeMode) => void; @@ -407,6 +417,7 @@ export function Composer({ onCwdChange, onBranchChange, onNewTerminal, + onAgentCommand, onModelChange, onModelSettingsChange, onRuntimeModeChange, @@ -491,10 +502,15 @@ export function Composer({ pickerOpen, }); const skills = skillCatalog.skills; + const [slashFilter, setSlashFilter] = useState(loadSlashFilter); + const [cliError, setCliError] = useState(""); + const cliSupported = !!onAgentCommand && supportsAgentCli(harness); + const effectiveFilter = cliSupported ? slashFilter : "all"; const slashItems = useMemo( () => [ PLAN_COMMAND, COMPACT_COMMAND, + ...(cliSupported ? agentCliCommands(harness) : []), ...skills.filter( (skill) => skill.kind === "native" || @@ -502,12 +518,17 @@ export function Composer({ skill.name !== COMPACT_COMMAND.name), ), ], - [skills], + [skills, cliSupported, harness], + ); + const skillLimit = + hasNativeCommands(harness) || cliSupported + ? Number.POSITIVE_INFINITY + : undefined; + const rankedSkills = rankSkills( + filterSlashItems(slashItems, effectiveFilter), + slash?.query ?? "", + skillLimit, ); - const skillLimit = hasNativeCommands(harness) - ? Number.POSITIVE_INFINITY - : undefined; - const rankedSkills = rankSkills(slashItems, slash?.query ?? "", skillLimit); const attachmentsSupported = harnessSupportsAttachments(harness); const skillNames = useMemo( () => new Set(slashItems.map((skill) => skill.invocation)), @@ -900,6 +921,31 @@ export function Composer({ }, [addAttachments, attachmentsSupported, enabled]); const submit = (value: string) => { + const native = cliSupported + ? agentCliPrompt(value, harness, effectiveFilter, skills) + : null; + if (native) { + // Keep attachments/cards/draft intact when a native session cannot open. + if (attachments.length || inboxCard || noteCard || handoffCard) { + setCliError( + "Send or remove the attached items first, or use the CLI button to keep this draft in chat.", + ); + return; + } + if (!onAgentCommand?.(native)) return; + setCliError(""); + if (ref.current) { + ref.current.value = ""; + ref.current.style.height = "auto"; + } + setDraft(""); + onDraftChange?.(""); + setSlash(null); + setMention(null); + setPlanSelected(false); + syncHasValue("", []); + return; + } if (isCompactCommand(value)) { if (!onCompactContext?.()) return; if (!ref.current) return; @@ -983,7 +1029,14 @@ export function Composer({ if ( e.key === "Enter" && !e.shiftKey && - isCompactCommand(e.currentTarget.value) + (isCompactCommand(e.currentTarget.value) || + (cliSupported && + agentCliPrompt( + e.currentTarget.value, + harness, + effectiveFilter, + skills, + ) !== null)) ) { e.preventDefault(); submit(e.currentTarget.value); @@ -1073,6 +1126,11 @@ export function Composer({ onResume={onResumeQueue} />
+ {cliError ? ( +

+ {cliError} +

+ ) : null} {pickerOpen ? (
{ + setSlashFilter(filter); + saveSlashFilter(filter); + setSkillActive(0); + ref.current?.focus(); + } + : undefined + } + onOpenCli={ + cliSupported + ? () => { + onAgentCommand?.(); + } + : undefined + } onStartCreate={() => { setCreatingSkill(true); setCreateError(null); @@ -1181,6 +1257,17 @@ export function Composer({ /> )}
+ {cliSupported ? ( + + ) : null} void; onCancelCreate: () => void; onCreate: (name: string, scope: "project" | "user") => void; + filter?: SlashFilter; + onFilterChange?: (filter: SlashFilter) => void; + onOpenCli?: () => void; }; export function SkillPicker({ @@ -42,6 +42,9 @@ export function SkillPicker({ onStartCreate, onCancelCreate, onCreate, + filter = "all", + onFilterChange, + onOpenCli, }: Props) { return (
) : ( <> + {onFilterChange ? ( +
+ {( + [ + ["all", "All"], + ["commands", "Agent commands"], + ["skills", "Skills"], + ] as const + ).map(([value, label]) => ( + + ))} +
+ ) : null} - + {onOpenCli && filter !== "skills" ? ( + + ) : null} + {filter !== "commands" ? ( + + ) : null} )}
@@ -174,9 +215,13 @@ function SkillList({ {skill.description} ) : null} - {skill.kind === "native" && (skill.inputHint || skill.subcommands?.length) ? ( + {skill.kind === "native" && + (skill.inputHint || skill.subcommands?.length) ? ( - {skill.inputHint || skill.subcommands?.map((sub) => sub.usage || sub.name).join(" · ")} + {skill.inputHint || + skill.subcommands + ?.map((sub) => sub.usage || sub.name) + .join(" · ")} ) : null} @@ -305,7 +350,9 @@ function ScopeButton({ disabled={disabled} onClick={onClick} className={`flex min-w-0 flex-1 flex-col rounded-md px-2 py-1.5 text-left ${ - selected ? "bg-content/20 text-content" : "bg-content/10 text-content/70" + selected + ? "bg-content/20 text-content" + : "bg-content/10 text-content/70" } disabled:opacity-40`} > {label} @@ -322,6 +369,7 @@ function scopeLabel(skill: Skill): string { } if (skill.kind === "builtin") return "monocode"; if (skill.scope === "user") return "personal"; - if (skill.source !== "agents" && skill.source !== "monocode") return skill.source; + if (skill.source !== "agents" && skill.source !== "monocode") + return skill.source; return "project"; } diff --git a/src/lib/agentCli.test.ts b/src/lib/agentCli.test.ts new file mode 100644 index 00000000..0e401efa --- /dev/null +++ b/src/lib/agentCli.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from "vitest"; +import { + agentCliCommands, + agentCliLaunch, + agentCliPrompt, + cliCommandInput, + filterSlashItems, +} from "./agentCli"; +import { PLAN_COMMAND } from "./plan"; +import { COMPACT_COMMAND } from "./compact"; +import type { Skill } from "./skills"; + +const skill: Skill = { + kind: "file", + name: "status", + invocation: "status", + description: "Project status", + path: "/repo/.agents/skills/status/SKILL.md", + scope: "project", + source: "agents", +}; + +describe("CLI command routing", () => { + it.each(["codex", "claude"] as const)( + "routes %s commands, arguments and new command names without sending them to the model", + (harness) => { + expect(agentCliPrompt("/status", harness, "all", [])).toBe("/status"); + expect(agentCliPrompt("/cli:model opus", harness, "all", [])).toBe( + "/model opus", + ); + expect( + agentCliPrompt("/future-command @file --flag", harness, "commands", []), + ).toBe("/future-command @file --flag"); + expect( + agentCliPrompt("Discuss /status", harness, "commands", []), + ).toBeNull(); + expect(agentCliPrompt("> /status", harness, "commands", [])).toBeNull(); + }, + ); + it("preserves skills and MonoCode commands and allows an explicit native choice", () => { + expect(agentCliPrompt("/status", "claude", "all", [skill])).toBeNull(); + expect(agentCliPrompt("/cli:status", "claude", "skills", [skill])).toBe( + "/status", + ); + expect(agentCliPrompt("/status", "claude", "commands", [skill])).toBe( + "/status", + ); + expect(agentCliPrompt("/compact", "codex", "all", [])).toBeNull(); + expect(agentCliPrompt("/compact", "codex", "commands", [])).toBe( + "/compact", + ); + expect(agentCliPrompt("/plan fix it", "codex", "all", [])).toBeNull(); + expect(agentCliPrompt("/cli:plan fix it", "codex", "all", [])).toBe( + "/plan fix it", + ); + expect(agentCliPrompt("/status", "codex", "skills", [])).toBeNull(); + expect(agentCliPrompt("/status", "omp", "commands", [])).toBeNull(); + }); + it("filters commands and skills without capping the available commands", () => { + const commands = agentCliCommands("claude"); + const items = [skill, PLAN_COMMAND, COMPACT_COMMAND, ...commands]; + expect(commands.length).toBeGreaterThan(50); + expect(filterSlashItems(items, "skills")).toEqual([skill]); + expect(filterSlashItems(items, "commands")).not.toContain(skill); + expect(filterSlashItems(items, "commands")).toEqual(commands); + expect(filterSlashItems(items, "all")).toEqual(items); + expect(new Set(commands.map((item) => item.invocation)).size).toBe( + commands.length, + ); + expect(agentCliCommands("cursor")).toEqual([]); + }); +}); + +describe("native CLI launch", () => { + it("forks existing history so CLI and chat never write the same conversation", () => { + const id = "a71674e4-0281-4c5e-9b77-0f4dc5e637ff"; + expect( + agentCliLaunch( + { harness: "codex", providerSessionId: id }, + "/path with spaces/codex", + ), + ).toEqual({ program: "/path with spaces/codex", args: ["fork", id] }); + expect( + agentCliLaunch( + { harness: "claude", providerSessionId: id }, + "/bin/claude", + ), + ).toEqual({ + program: "/bin/claude", + args: ["--resume", id, "--fork-session"], + }); + }); + it("starts fresh when a provider switch makes the old session incompatible", () => { + expect( + agentCliLaunch( + { + harness: "claude", + providerSessionId: "old-codex-id", + pendingSwitch: { + from: "codex", + fromModel: "codex:test", + fromSettings: {}, + }, + }, + "/bin/claude", + ).args, + ).toEqual([]); + expect(agentCliLaunch({ harness: "codex" }, "/bin/codex").args).toEqual([]); + }); + it("does not interpret user data as shell code or use permission bypass flags", () => { + const launch = agentCliLaunch( + { harness: "codex", providerSessionId: "literal; $(do-not-run)" }, + "/bin/codex", + ); + expect(launch.args).toEqual(["fork", "literal; $(do-not-run)"]); + expect(launch.args.join(" ")).not.toContain("bypass"); + expect(() => agentCliLaunch({ harness: "cursor" }, "/bin/agent")).toThrow(); + }); + it("rejects terminal escapes and extra Enter presses while preserving legitimate command arguments", () => { + expect(cliCommandInput('/model "some model"')).toBe('/model "some model"'); + for (const value of [ + "/status\r/logout", + "/status\n/model", + "/status\x1b[A", + "/status\t", + "status", + "/status\0", + ]) { + expect(() => cliCommandInput(value)).toThrow(); + } + }); +}); diff --git a/src/lib/agentCli.ts b/src/lib/agentCli.ts new file mode 100644 index 00000000..0c6e3698 --- /dev/null +++ b/src/lib/agentCli.ts @@ -0,0 +1,120 @@ +import codexCommands from "./codexCliCommands.json"; +import claudeCommands from "./claudeCliCommands.json"; +import type { HarnessId, Session } from "./session"; +import type { Skill } from "./skills"; +import type { PtyCommand } from "./pty"; + +export type AgentCliHarness = "codex" | "claude"; +export type SlashFilter = "all" | "commands" | "skills"; +const FILTER_KEY = "monocode.slashFilter"; + +export function supportsAgentCli( + harness: HarnessId, +): harness is AgentCliHarness { + return harness === "codex" || harness === "claude"; +} + +// Documented command names, 2026-09-06. The installed CLI owns availability, +// execution, custom commands and future commands; these are completion hints. +// https://learn.chatgpt.com/docs/developer-commands +// https://code.claude.com/docs/en/commands +export function agentCliCommands(harness: HarnessId): Skill[] { + if (!supportsAgentCli(harness)) return []; + const names = harness === "codex" ? codexCommands : claudeCommands; + return names.map((name) => ({ + kind: "native", + name, + invocation: `cli:${name}`, + aliases: [name], + description: `Open /${name} in ${harness === "codex" ? "Codex" : "Claude Code"} CLI`, + source: harness, + origin: "CLI", + })); +} + +export function loadSlashFilter(): SlashFilter { + try { + const value = localStorage.getItem(FILTER_KEY); + return value === "commands" || value === "skills" ? value : "all"; + } catch { + return "all"; + } +} + +export function saveSlashFilter(filter: SlashFilter): void { + try { + localStorage.setItem(FILTER_KEY, filter); + } catch { + /* private mode */ + } +} + +export function filterSlashItems(items: Skill[], filter: SlashFilter): Skill[] { + return items.filter( + (item) => + filter === "all" || + (filter === "commands" + ? item.kind === "native" + : item.kind === "file" || item.name === "create-skill"), + ); +} + +/** Explicit /cli:name works in every filter. Skills and MonoCode shortcuts + * retain their existing meaning unless the user selects Agent commands. */ +export function agentCliPrompt( + text: string, + harness: HarnessId, + filter: SlashFilter, + skills: Skill[], +): string | null { + if (!supportsAgentCli(harness)) return null; + const match = text.match(/^\s*\/([a-zA-Z0-9_.:-]+)(?=\s|$)/); + if (!match) return null; + const name = match[1]; + if (name.startsWith("cli:") && name.length > 4) { + return text.trimStart().replace(/^\/cli:/, "/"); + } + if (filter === "skills") return null; + if (filter === "commands") return text.trimStart(); + if ( + name === "plan" || + name === "compact" || + skills.some((skill) => skill.invocation === name) + ) + return null; + const names = harness === "codex" ? codexCommands : claudeCommands; + return names.includes(name) ? text.trimStart() : null; +} + +/** Native sessions are forks, never a second writer to the chat's history. + * Pass argv directly to the PTY host; no shell quoting or prompt argument. */ +export function agentCliLaunch( + session: Pick, + binary: string, +): PtyCommand { + if (!supportsAgentCli(session.harness)) + throw new Error("This agent has no native CLI view."); + if (!binary.trim()) throw new Error("Agent executable was not found."); + const resume = !session.pendingSwitch && session.providerSessionId; + return { + program: binary, + args: resume + ? session.harness === "codex" + ? ["fork", resume] + : ["--resume", resume, "--fork-session"] + : [], + }; +} + +/** Terminal escape sequences must never become keyboard input from a draft. */ +export function cliCommandInput(text: string): string { + if ( + !/^\/[a-zA-Z0-9_.:-]+(?:\s[^\r\n]*)?$/.test(text.trim()) || + /[\x00-\x1f\x7f]/.test(text) + ) { + throw new Error( + "Enter one slash command without line breaks or control characters.", + ); + } + return text.trim(); +} diff --git a/src/lib/claudeCliCommands.json b/src/lib/claudeCliCommands.json new file mode 100644 index 00000000..1eec13d3 --- /dev/null +++ b/src/lib/claudeCliCommands.json @@ -0,0 +1,114 @@ +[ + "add-dir", + "advisor", + "agents", + "artifacts", + "auto-mode-setup", + "autocompact", + "autofix-pr", + "background", + "batch", + "branch", + "btw", + "bug", + "cd", + "chrome", + "claude-api", + "clear", + "code-review", + "color", + "compact", + "config", + "context", + "copy", + "cost", + "dataviz", + "debug", + "deep-research", + "design", + "design-login", + "design-sync", + "desktop", + "diff", + "doctor", + "effort", + "exit", + "export", + "fast", + "feedback", + "fewer-permission-prompts", + "focus", + "fork", + "goal", + "heapdump", + "help", + "hooks", + "ide", + "import", + "init", + "insights", + "install-github-app", + "install-slack-app", + "keybindings", + "list-agents", + "login", + "logout", + "loop", + "mcp", + "memory", + "mobile", + "model", + "passes", + "permissions", + "plan", + "plugin", + "powerup", + "pr-comments", + "privacy-settings", + "radio", + "rate-limit-options", + "recap", + "release-notes", + "reload-plugins", + "reload-skills", + "remote-control", + "remote-env", + "rename", + "resume", + "review", + "rewind", + "run", + "run-skill-generator", + "sandbox", + "schedule", + "scroll-speed", + "security-review", + "setup-bedrock", + "setup-vertex", + "simplify", + "skill-doctor", + "skills", + "stats", + "status", + "statusline", + "stickers", + "stop", + "subtask", + "tasks", + "team-onboarding", + "teleport", + "terminal-setup", + "theme", + "tui", + "ultraplan", + "ultrareview", + "upgrade", + "usage", + "usage-credits", + "verify", + "vim", + "voice", + "web-setup", + "workflow-authoring", + "workflows" +] diff --git a/src/lib/codexCliCommands.json b/src/lib/codexCliCommands.json new file mode 100644 index 00000000..331e1941 --- /dev/null +++ b/src/lib/codexCliCommands.json @@ -0,0 +1,59 @@ +[ + "permissions", + "ide", + "keymap", + "vim", + "setup-default-sandbox", + "sandbox-add-read-dir", + "agent", + "apps", + "plugins", + "hooks", + "clear", + "rename", + "archive", + "delete", + "compact", + "copy", + "diff", + "exit", + "experimental", + "approve", + "memories", + "skills", + "import", + "feedback", + "init", + "logout", + "mcp", + "mention", + "model", + "fast", + "plan", + "goal", + "personality", + "ps", + "stop", + "fork", + "app", + "side", + "raw", + "resume", + "new", + "quit", + "review", + "status", + "usage", + "debug-config", + "statusline", + "title", + "theme", + "pets", + "cloud", + "cloud-environment", + "ide-context", + "local", + "project", + "reasoning", + "worktree" +] diff --git a/src/lib/pty.ts b/src/lib/pty.ts index 4ed47c44..e74b072e 100644 --- a/src/lib/pty.ts +++ b/src/lib/pty.ts @@ -7,6 +7,8 @@ type ExitPayload = { id: string; code: number | null }; type DataHandler = (data: Uint8Array) => void; type ExitHandler = (code: number | null) => void; +export type PtyCommand = { program: string; args: string[] }; + const dataHandlers = new Map(); const exitHandlers = new Map(); const dataBuffer = new Map(); @@ -118,8 +120,17 @@ export async function spawnPty( cwd: string, cols: number, rows: number, + command?: PtyCommand, ): Promise { - await invoke("pty_spawn", { id, cwd, cols, rows }); + ensureBridge(); + await bridge; + await invoke("pty_spawn", { + id, + cwd, + cols, + rows, + ...(command ? { command } : {}), + }); } export async function writePty(id: string, data: string): Promise { diff --git a/src/surfaces/AgentCliView.tsx b/src/surfaces/AgentCliView.tsx new file mode 100644 index 00000000..509f6e47 --- /dev/null +++ b/src/surfaces/AgentCliView.tsx @@ -0,0 +1,155 @@ +import { useEffect, useRef, useState } from "react"; +import { resolveClaudeBinary, resolveCodexBinary } from "../lib/harness/child"; +import { + agentCliLaunch, + cliCommandInput, + type AgentCliHarness, +} from "../lib/agentCli"; +import { sessionWorkCwd, type Session } from "../lib/session"; +import { killPty, type PtyCommand } from "../lib/pty"; +import { TerminalView } from "./TerminalView"; + +type Props = { + session: Session & { harness: AgentCliHarness }; + initialCommand?: string; + active: boolean; + onClose: () => void; +}; + +/** Run the real installed agent: every native menu, plugin and future slash + * command remains owned by that agent. Never emulate commands as model text. */ +export function AgentCliView({ + session, + initialCommand = "", + active, + onClose, +}: Props) { + const [launch] = useState(() => ({ + id: `agent-cli-${crypto.randomUUID()}`, + cwd: sessionWorkCwd(session), + session, + })); + const [command, setCommand] = useState(); + const [draft, setDraft] = useState(initialCommand); + const [error, setError] = useState(""); + const [ready, setReady] = useState(false); + const [exited, setExited] = useState(false); + const [sending, setSending] = useState(false); + const [closing, setClosing] = useState(false); + const sender = useRef<((text: string) => Promise) | null>(null); + const name = session.harness === "codex" ? "Codex" : "Claude Code"; + const copied = + !!launch.session.providerSessionId && !launch.session.pendingSwitch; + + useEffect(() => { + let cancelled = false; + const resolve = + launch.session.harness === "codex" + ? resolveCodexBinary + : resolveClaudeBinary; + void resolve() + .then(({ path }) => { + if (!cancelled) setCommand(agentCliLaunch(launch.session, path)); + }) + .catch((err: unknown) => { + if (!cancelled) + setError(err instanceof Error ? err.message : String(err)); + }); + return () => { + cancelled = true; + }; + }, [launch]); + + const send = async () => { + if (!sender.current || sending || closing) return; + try { + const text = cliCommandInput(draft); + setSending(true); + setError(""); + await sender.current(text); + setDraft(""); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setSending(false); + } + }; + + return ( +
+
+ {name} CLI + + {copied + ? "Separate session with a copy of this chat." + : "Separate CLI session in this project."}{" "} + Type / in the terminal for its full command menu. + + +
+ {draft ? ( +
{ + event.preventDefault(); + void send(); + }} + > + setDraft(event.target.value)} + className="min-w-0 flex-1 rounded bg-content/5 px-2 py-1 font-mono text-sm" + /> + + Finish any startup prompts below first. + + +
+ ) : null} + {error ? ( +

+ {error} +

+ ) : null} +
+ {command ? ( + { + sender.current = sendCommand; + setReady(!!sendCommand); + }} + onExit={() => setExited(true)} + /> + ) : ( +

+ {error ? "CLI could not start." : "Starting CLI…"} +

+ )} +
+
+ ); +} diff --git a/src/surfaces/SessionPane.tsx b/src/surfaces/SessionPane.tsx index 3bff1dbe..6f7daf6f 100644 --- a/src/surfaces/SessionPane.tsx +++ b/src/surfaces/SessionPane.tsx @@ -42,6 +42,8 @@ import { loadNotesEnabled, subscribeNotesEnabled } from "../lib/settings"; import { resolveModel } from "../lib/models"; import { isAstraModel } from "../lib/astraWelcome"; import { AstraWelcome } from "./AstraWelcome"; +import { AgentCliView } from "./AgentCliView"; +import { supportsAgentCli, cliCommandInput } from "../lib/agentCli"; type Props = { session: Session; @@ -238,11 +240,34 @@ export const SessionPane = memo(function SessionPane({ const showDeckProjectPicker = isEmpty && !looksLikeProject(session.cwd); const dockComposer = !isEmpty || inSplit || !!session.inboxAsk; const draftRef = useRef(undefined); + const [nativeCli, setNativeCli] = useState<{ command?: string } | null>(null); + const [nativeError, setNativeError] = useState(""); + const openAgentCli = (command?: string): boolean => { + if ( + session.busy || + session.queuedMessages?.length || + !supportsAgentCli(session.harness) + ) { + setNativeError( + "Finish the current turn and queued messages before opening the CLI.", + ); + return false; + } + try { + if (command) cliCommandInput(command); + setNativeError(""); + setNativeCli({ command }); + return true; + } catch (error) { + setNativeError(error instanceof Error ? error.message : String(error)); + return false; + } + }; const composer = ( onCwdChange(session.id, cwd)} onBranchChange={() => onBranchChange(session.id)} onNewTerminal={() => onNewTerminal(session.id)} + onAgentCommand={ + supportsAgentCli(session.harness) ? openAgentCli : undefined + } onModelChange={(harness, model) => { onModelChange(session.id, harness, model); const selected = resolveModel(harness, model); @@ -337,120 +365,139 @@ export const SessionPane = memo(function SessionPane({ className="relative isolate flex h-full min-h-0 min-w-0 flex-1 flex-col" onMouseDown={() => onFocus(session.id)} > - {astraWelcomeRun !== null && visible ? ( - + {nativeCli && supportsAgentCli(session.harness) ? ( + setNativeCli(null)} + /> ) : null} - {inSplit ? ( -
{ - if (event.button !== 0 || !onPaneDragStart) return; - if ( - (event.target as HTMLElement | null)?.closest("[data-no-drag]") - ) { - return; - } - onPaneDragStart(event); - }} - > - {onPaneDragStart ? ( - - ) : null} - - - {title} - - -
- ) : null} -
- {isEmpty ? ( - session.inboxAsk ? ( -
- -
- ) : ( - - ) - ) : ( - <> - - onSecondOpinion(session.id, harness, turn, model) - : undefined - } - onHandoff={ - !session.inboxAsk && onHandoff - ? (harness, turn, model) => - onHandoff(session.id, harness, turn, model) - : undefined - } - onJumpToBottomChange={setShowJumpToBottom} - onJumpToBottomReady={onJumpToBottomReady} + {onPaneDragStart ? ( + + ) : null} + - {showJumpToBottom ? ( -
- + + {title} + + +
+ ) : null} +
+ {isEmpty ? ( + session.inboxAsk ? ( +
+
- ) : null} - - )} + ) : ( + + ) + ) : ( + <> + + onSecondOpinion(session.id, harness, turn, model) + : undefined + } + onHandoff={ + !session.inboxAsk && onHandoff + ? (harness, turn, model) => + onHandoff(session.id, harness, turn, model) + : undefined + } + onJumpToBottomChange={setShowJumpToBottom} + onJumpToBottomReady={onJumpToBottomReady} + /> + {showJumpToBottom ? ( +
+ +
+ ) : null} + + )} +
+ {dockComposer ? ( +
{composer}
+ ) : null}
- {dockComposer ? ( -
{composer}
- ) : null}
); }); diff --git a/src/surfaces/TerminalView.tsx b/src/surfaces/TerminalView.tsx index ef4e17fc..a196b67b 100644 --- a/src/surfaces/TerminalView.tsx +++ b/src/surfaces/TerminalView.tsx @@ -7,6 +7,7 @@ import { spawnPty, subscribePty, writePty, + type PtyCommand, } from "../lib/pty"; import { isOscColorQuery, oscColorReply } from "../lib/terminalChrome"; import { @@ -29,6 +30,9 @@ type Props = { cwd: string; active: boolean; onMetaChange?: (patch: TerminalMetaPatch) => void; + command?: PtyCommand; + onReady?: (send: ((text: string) => Promise) | null) => void; + onExit?: (code: number | null) => void; }; function cssColor(expr: string, fallback: string): string { @@ -108,7 +112,15 @@ function oscColors() { return isLightScheme() ? OSC_LIGHT : OSC_DARK; } -export function TerminalView({ id, cwd, active, onMetaChange }: Props) { +export function TerminalView({ + id, + cwd, + active, + onMetaChange, + command, + onReady, + onExit, +}: Props) { const outerRef = useRef(null); const hostRef = useRef(null); const termRef = useRef(null); @@ -117,6 +129,10 @@ export function TerminalView({ id, cwd, active, onMetaChange }: Props) { const onMetaChangeRef = useRef(onMetaChange); onMetaChangeRef.current = onMetaChange; const runningProcessRef = useRef(null); + const readyRef = useRef(onReady); + readyRef.current = onReady; + const exitRef = useRef(onExit); + exitRef.current = onExit; useEffect(() => { const outer = outerRef.current; @@ -190,14 +206,34 @@ export function TerminalView({ id, cwd, active, onMetaChange }: Props) { }, (code) => { if (closed) return; + spawned.current = false; + readyRef.current?.(null); + exitRef.current?.(code); const status = code == null ? "" : ` (${code})`; term.writeln(`\r\n[process exited${status}]`); }, ); - const starting = spawnPty(id, cwd, term.cols, term.rows) + const starting = spawnPty(id, cwd, term.cols, term.rows, command) .then(() => { - if (!closed) spawned.current = true; + if (!closed) { + spawned.current = true; + readyRef.current?.(async (text) => { + if (closed || !spawned.current) + throw new Error("The CLI has exited."); + const input = term.modes.bracketedPasteMode + ? `\x1b[200~${text}\x1b[201~` + : text; + await writePty(id, input); + // Native TUIs coalesce bursts into paste events. Give their event + // loop time to accept the draft before delivering a separate Enter. + await new Promise((resolve) => setTimeout(resolve, 150)); + if (closed || !spawned.current) + throw new Error("The CLI has exited."); + await writePty(id, "\r"); + term.focus(); + }); + } }) .catch((error) => { spawned.current = false; @@ -300,6 +336,7 @@ export function TerminalView({ id, cwd, active, onMetaChange }: Props) { return () => { closed = true; + readyRef.current?.(null); cancelAnimationFrame(frame); if (raf) cancelAnimationFrame(raf); observer.disconnect(); @@ -320,7 +357,7 @@ export function TerminalView({ id, cwd, active, onMetaChange }: Props) { termRef.current = null; spawned.current = false; }; - }, [id, cwd]); + }, [id, cwd, command]); // Identity-stable: the callers pass an inline arrow, so depending on the // prop itself would tear down and re-arm the poll — and re-fork `ps` — on diff --git a/tests/agent-cli.html b/tests/agent-cli.html new file mode 100644 index 00000000..9a40cc42 --- /dev/null +++ b/tests/agent-cli.html @@ -0,0 +1,11 @@ + + + + + MonoCode CLI test + + +
+ + + diff --git a/tests/agent-cli.tsx b/tests/agent-cli.tsx new file mode 100644 index 00000000..5a856f48 --- /dev/null +++ b/tests/agent-cli.tsx @@ -0,0 +1,100 @@ +// Browser smoke fixture. IPC is mocked; it never starts a real agent or model turn. +import React, { useState } from "react"; +import { createRoot } from "react-dom/client"; +import { mockIPC, mockWindows } from "@tauri-apps/api/mocks"; +import { emit } from "@tauri-apps/api/event"; +import "../src/index.css"; +const calls: unknown[] = []; +Object.assign(window, { testCalls: calls }); +mockWindows("main"); +mockIPC( + (cmd, args) => { + calls.push({ cmd, args }); + if (cmd === "list_skills") + return [ + { + name: "research", + description: "Research skill", + path: "/repo/.agents/skills/research/SKILL.md", + scope: "project", + source: "agents", + }, + ]; + if (cmd === "home_dir" || cmd === "default_cwd") return "/repo"; + if (cmd.startsWith("harness_resolve_")) return { path: "/test/agent" }; + if ( + cmd === "list_project_files" || + cmd.includes("list_") || + cmd.includes("read_dir") + ) + return []; + if (cmd === "pty_spawn") { + setTimeout(() => { + void emit("pty-data", { + id: args?.id, + data: btoa("\x1b[?2004hAgent ready. Type / for commands.\r\n> "), + }); + }, 100); + return; + } + if (cmd === "pty_status") return { foreground: "agent" }; + return null; + }, + { shouldMockEvents: true }, +); +const { Composer } = await import("../src/chrome/Composer"); +const { AgentCliView } = await import("../src/surfaces/AgentCliView"); +const { newSession } = await import("../src/lib/session"); +function Fixture() { + const [harness, setHarness] = useState<"codex" | "claude">("codex"); + const [native, setNative] = useState<{ command?: string } | null>(null); + const [session] = useState(() => + newSession("codex", "/repo", "codex:gpt-6-astra"), + ); + return ( +
+ + {native ? ( + setNative(null)} + /> + ) : null} +
+ {}} + onCwdChange={() => {}} + onModelChange={() => {}} + onRuntimeModeChange={() => {}} + onSubmit={(text) => calls.push({ modelTurn: text })} + onAgentCommand={(command) => { + calls.push({ agentCommand: command ?? "" }); + setNative({ command }); + return true; + }} + /> +
+
+ ); +} +createRoot(document.getElementById("root")!).render(); From 47751a886a02900199c4a24814ab279f597cfb00 Mon Sep 17 00:00:00 2001 From: lmanualm Date: Mon, 7 Sep 2026 12:58:47 +0200 Subject: [PATCH 2/2] Keep agent commands in the current chat interface --- docs/agent-cli.md | 95 +++--- docs/validation/agent-cli-menu.png | Bin 40463 -> 0 bytes docs/validation/agent-commands-chat.png | Bin 0 -> 41810 bytes src-tauri/src/pty.rs | 48 +-- src/chrome/AgentCommandPanel.tsx | 191 +++++++++++ src/chrome/Composer.tsx | 101 +++--- src/chrome/SkillPicker.tsx | 14 +- src/lib/agentCli.test.ts | 132 -------- src/lib/agentCli.ts | 120 ------- src/lib/agentCommands.test.ts | 225 +++++++++++++ src/lib/agentCommands.ts | 279 ++++++++++++++++ src/lib/pty.ts | 13 +- src/surfaces/AgentCliView.tsx | 155 --------- src/surfaces/SessionPane.tsx | 303 +++++++++--------- src/surfaces/TerminalView.tsx | 45 +-- tests/agent-cli.tsx | 100 ------ tests/{agent-cli.html => agent-commands.html} | 4 +- tests/agent-commands.tsx | 167 ++++++++++ 18 files changed, 1129 insertions(+), 863 deletions(-) delete mode 100644 docs/validation/agent-cli-menu.png create mode 100644 docs/validation/agent-commands-chat.png create mode 100644 src/chrome/AgentCommandPanel.tsx delete mode 100644 src/lib/agentCli.test.ts delete mode 100644 src/lib/agentCli.ts create mode 100644 src/lib/agentCommands.test.ts create mode 100644 src/lib/agentCommands.ts delete mode 100644 src/surfaces/AgentCliView.tsx delete mode 100644 tests/agent-cli.tsx rename tests/{agent-cli.html => agent-commands.html} (51%) create mode 100644 tests/agent-commands.tsx diff --git a/docs/agent-cli.md b/docs/agent-cli.md index dd904505..ae269703 100644 --- a/docs/agent-cli.md +++ b/docs/agent-cli.md @@ -1,43 +1,54 @@ -# Agent CLI commands - -For Codex and Claude Code, type `/` and choose **All**, **Agent commands**, or -**Skills**. This choice is remembered. **Agent commands** accepts any slash -command, including custom and newly released commands absent from completion. - -Use `/cli:status`, `/cli:model`, or `/cli:compact` to explicitly select the -agent's command in any filter. In **All**, existing skills and MonoCode's -`/plan` and `/compact` retain their meaning. In **Skills**, existing skill -submission is unchanged. The **CLI** button opens the native agent directly. - -Native commands open an embedded terminal running the installed CLI. Complete -its startup prompts, then click **Send to CLI** to send the prepared command, -or type directly in the terminal. Type `/` there for the agent's own complete -menu. Its menus, custom commands, plugins, login, permissions, model settings -and future commands are handled by the installed agent. The suggestion catalog -contains documented command names; availability can differ by agent version, -platform, account, and enabled features. - -The native session is **separate**. If this chat has provider history, Codex -uses `fork ` and Claude uses `--resume --fork-session` to copy it. This -prevents two processes from writing the same conversation. Messages and -per-session settings are not synchronized back into the original MonoCode -chat. Project files and the agent account are shared: file edits, sign-out and -global configuration changes still affect other sessions. Use the agent's -`/resume` to reopen native history. -New chats and pending provider switches start a fresh native session. Native -CLI settings apply, including its default model and permission policy. - -The regular composer stays mounted so drafts and attachments survive a visit -to CLI. Opening a CLI is blocked during an active turn or queued follow-ups. -**Close CLI · Back to chat** terminates only that embedded process tree. Closing -the MonoCode pane or app also cleans it up. Closing may interrupt work in that -native session; use `/exit` in the agent when you want it to finish gracefully. - -Validation: `npm run check:web`, `npm run check:rust`. The browser smoke fixture -at `/tests/agent-cli.html` (serve with `npm run dev`) mocks IPC and cannot start -an agent or model turn. It covers the real composer and native-terminal UI. - -Command-name references: - -- [Codex commands](https://learn.chatgpt.com/docs/developer-commands) +# Agent commands in chat + +For Codex and Claude Code, the `/` picker has remembered **All**, **Agent +commands**, and **Skills** filters. Commands operate on the current MonoCode +conversation. Their results and controls appear above the composer while the +transcript, draft, and attachments stay mounted. + +Use `/agent:status` or `/agent:model` to choose a command explicitly when a skill +has the same name. `/cli:name` is accepted as a compatibility alias and has the +same chat behavior. In **All**, skills and MonoCode's `/plan` and `/compact` keep +their existing meaning. **Skills** retains normal skill submission. + +| Command | Behavior in MonoCode | +| --- | --- | +| `/status` | Show this chat's model, access mode, project, provider session ID, state, and reported context usage. | +| `/context` | Show the last context reading reported by the agent. | +| `/model [model ID]` | Open a model selector or select an exact ID/name from this agent's catalog. | +| `/permissions [mode]`, `/approvals [mode]` | Show or change this chat's MonoCode access mode. | +| `/compact` | Compact the existing conversation through its provider adapter. | +| `/plan [request]` | Plan the next message, or send the supplied request in plan mode. | +| `/fast [on\|off]` | Show or set fast mode when the current model's catalog exposes it. | +| `/effort [level]`, `/reasoning [level]` | Show or set an effort level advertised by the current model. | +| `/diff` | Open this session's project diff in MonoCode. | +| `/stop` | Stop the current agent turn. | +| `/help` | Show the supported chat commands. | + +Access modes use MonoCode's existing names: `supervised`, `auto-accept-edits`, +`auto`, and `full-access`. Settings apply to the same conversation through the +same callbacks as the normal composer controls. Settings changes and compaction +are blocked during an active turn or queued follow-ups; status and stop remain +available. + +This is a GUI command integration, not full CLI command parity. The picker also +recognizes documented CLI names, but entries without a chat implementation are +marked **Not supported in chat**. Selecting one shows an error and preserves the +command draft. Unknown names in **Agent commands** receive the same treatment. +No command starts a terminal, forks provider history, or falls back to sending +unsupported command text as a model prompt. `/plan ` intentionally +starts a normal planning turn, and `/compact` invokes provider compaction. + +`/status` displays MonoCode's current session data, not the terminal's native +status screen. Account rate limits remain in the existing usage footer; unknown +context usage is shown as unavailable rather than zero. + +Validate with `npm run check`. The browser regression fixture at +`/tests/agent-commands.html` (serve with `npm run dev`) uses the real SessionPane, +composer, and transcript with mocked IPC. It cannot launch a real agent or PTY. + +Protocol and command references: + +- [Codex app-server](https://learn.chatgpt.com/docs/app-server) +- [Codex CLI commands](https://learn.chatgpt.com/docs/developer-commands) +- [Claude Agent SDK commands](https://code.claude.com/docs/en/agent-sdk/slash-commands#commands-in-agent-sdk-sessions) - [Claude Code commands](https://code.claude.com/docs/en/commands) diff --git a/docs/validation/agent-cli-menu.png b/docs/validation/agent-cli-menu.png deleted file mode 100644 index 407cc89401e2be29888b4da057727f3698853b55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40463 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 diff --git a/docs/validation/agent-commands-chat.png b/docs/validation/agent-commands-chat.png new file mode 100644 index 0000000000000000000000000000000000000000..d7d63e6bfa473cd28fa2d1b51d842825eea29781 GIT binary patch literal 41810 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 IID74X0e4PDI{*Lx literal 0 HcmV?d00001 diff --git a/src-tauri/src/pty.rs b/src-tauri/src/pty.rs index fe36acfe..8686f516 100644 --- a/src-tauri/src/pty.rs +++ b/src-tauri/src/pty.rs @@ -8,7 +8,7 @@ use std::time::Duration; #[cfg(unix)] use std::time::Instant; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use tauri::{AppHandle, Emitter, Manager, State}; use crate::dirs_home; @@ -50,24 +50,6 @@ pub struct PtyHost { sessions: Mutex>>, } -#[derive(Deserialize)] -pub struct PtyCommand { - program: String, - args: Vec, -} - -impl PtyCommand { - fn validate(&self) -> Result<(), String> { - if self.program.trim().is_empty() - || self.program.contains('\0') - || self.args.iter().any(|arg| arg.contains('\0')) - { - return Err("Invalid terminal command.".into()); - } - Ok(()) - } -} - impl PtyHost { pub fn new() -> Self { Self { @@ -141,11 +123,7 @@ pub fn pty_spawn( cwd: String, cols: u16, rows: u16, - command: Option, ) -> Result<(), String> { - if let Some(command) = &command { - command.validate()?; - } if let Some(prev) = host.remove(&id) { terminate(prev.pid); #[cfg(unix)] @@ -154,17 +132,17 @@ pub fn pty_spawn( #[cfg(unix)] { - spawn_unix(app, host, id, cwd, cols.max(2), rows.max(2), command) + spawn_unix(app, host, id, cwd, cols.max(2), rows.max(2)) } #[cfg(windows)] { - spawn_windows(app, host, id, cwd, cols.max(2), rows.max(2), command) + spawn_windows(app, host, id, cwd, cols.max(2), rows.max(2)) } #[cfg(not(any(unix, windows)))] { - let _ = (app, cwd, cols, rows, command); + let _ = (app, cwd, cols, rows); Err("Terminals are not supported on this platform.".into()) } } @@ -260,7 +238,6 @@ fn spawn_unix( cwd: String, cols: u16, rows: u16, - command: Option, ) -> Result<(), String> { use std::fs::File; use std::os::unix::io::FromRawFd; @@ -268,9 +245,7 @@ fn spawn_unix( use std::process::Command; let workdir = working_dir(&cwd); - let (shell, args) = command - .map(|cmd| (cmd.program, cmd.args)) - .unwrap_or_else(default_shell); + let (shell, args) = default_shell(); let (master, slave) = open_pty(cols, rows)?; let mut cmd = Command::new(&shell); @@ -307,12 +282,10 @@ fn spawn_unix( }); } - let result = cmd.spawn(); + let mut child = cmd + .spawn() + .map_err(|e| format!("Failed to start {shell}: {e}"))?; close_fd(slave); - let mut child = result.map_err(|e| { - close_fd(master); - format!("Failed to start {shell}: {e}") - })?; let pid = child.id(); set_cloexec(master); @@ -394,14 +367,11 @@ fn spawn_windows( cwd: String, cols: u16, rows: u16, - command: Option, ) -> Result<(), String> { use portable_pty::{native_pty_system, CommandBuilder, PtySize}; let workdir = working_dir(&cwd); - let (shell, args) = command - .map(|cmd| (cmd.program, cmd.args)) - .unwrap_or_else(default_shell); + let (shell, args) = default_shell(); let pty_system = native_pty_system(); let pair = pty_system .openpty(PtySize { diff --git a/src/chrome/AgentCommandPanel.tsx b/src/chrome/AgentCommandPanel.tsx new file mode 100644 index 00000000..2479dd84 --- /dev/null +++ b/src/chrome/AgentCommandPanel.tsx @@ -0,0 +1,191 @@ +import { useSyncExternalStore } from "react"; +import { X } from "./icons"; +import { ModelSettings } from "./ModelSettings"; +import { + CHAT_COMMANDS, + commandModel, + commandsLocked, + type CommandPanel, +} from "../lib/agentCommands"; +import { getModelSnapshot, modelsFor, subscribeModels } from "../lib/models"; +import { contextTooltip } from "../lib/contextUsage"; +import { + RUNTIME_MODES, + RUNTIME_MODE_LABEL, + RUNTIME_MODE_HINT, + sessionWorkCwd, + type RuntimeMode, + type Session, +} from "../lib/session"; + +type Props = { + panel: CommandPanel; + session: Session; + onClose: () => void; + onModelChange: (model: string) => void; + onModelSettingsChange: (settings: Record) => void; + onRuntimeModeChange: (mode: RuntimeMode) => void; +}; + +/** A small response/control panel in the existing composer. It never replaces + * the transcript or changes the identity of the conversation. */ +export function AgentCommandPanel({ + panel, + session, + onClose, + onModelChange, + onModelSettingsChange, + onRuntimeModeChange, +}: Props) { + useSyncExternalStore(subscribeModels, getModelSnapshot, getModelSnapshot); + const model = commandModel(session); + const context = session.context ? contextTooltip(session.context) : null; + const locked = commandsLocked(session); + return ( +
+
+ /{panel.command} + + {session.harness === "codex" ? "Codex" : "Claude Code"} · current chat + + +
+ {panel.message ? ( +

+ {panel.message} +

+ ) : null} + {panel.kind === "status" || panel.kind === "context" ? ( +
+ {panel.kind === "status" ? ( + <> +
Model
+
+ {model?.name || session.model || "Not selected"} +
+
Access
+
{RUNTIME_MODE_LABEL[session.runtimeMode]}
+
Project
+
{sessionWorkCwd(session)}
+
Session
+
+ {session.providerSessionId || "Starts with the first message"} +
+
State
+
+ {session.busy ? "Working" : "Idle"} + {session.queuedMessages?.length + ? ` · ${session.queuedMessages.length} queued` + : ""} +
+ + ) : null} +
Context
+
+ {context + ? `${context.headline} · ${context.detail}` + : "Not reported by the agent yet"} +
+
+ ) : null} + {panel.kind === "model" ? ( + + ) : null} + {panel.kind === "permissions" ? ( +
+ {RUNTIME_MODES.map((mode) => ( + + ))} +
+ ) : null} + {panel.kind === "settings" ? ( +
+ { + if (!locked) onModelSettingsChange(settings); + }} + /> +
+ ) : null} + {["model", "permissions", "settings"].includes(panel.kind) && locked ? ( +

+ Finish the current turn and queued messages to change settings. +

+ ) : null} + {panel.kind === "help" ? ( + <> +

+ These commands act on this chat. Use /agent:name when a skill has + the same name. Other CLI commands are marked as unsupported in chat. +

+
+ {Object.entries(CHAT_COMMANDS).map(([name, description]) => ( +
+
/{name}
+
{description}
+
+ ))} +
+ + ) : null} +
+ ); +} diff --git a/src/chrome/Composer.tsx b/src/chrome/Composer.tsx index 6e134c67..d9e2f39a 100644 --- a/src/chrome/Composer.tsx +++ b/src/chrome/Composer.tsx @@ -122,14 +122,14 @@ import { Popover } from "./Popover"; import { consumePlanCommand, PLAN_COMMAND } from "../lib/plan"; import { COMPACT_COMMAND, isCompactCommand } from "../lib/compact"; import { - agentCliCommands, - agentCliPrompt, + agentCommands, + agentCommandPrompt, filterSlashItems, loadSlashFilter, saveSlashFilter, - supportsAgentCli, + supportsAgentCommands, type SlashFilter, -} from "../lib/agentCli"; +} from "../lib/agentCommands"; type Props = { enabled?: boolean; @@ -163,7 +163,8 @@ type Props = { onCwdChange: (cwd: string) => void; onBranchChange?: () => void; onNewTerminal?: () => void; - onAgentCommand?: (command?: string) => boolean; + onAgentCommand?: (command: string) => boolean; + commandResult?: ReactNode; onModelChange: (harness: HarnessId, model: string) => void; onModelSettingsChange?: (settings: Record) => void; onRuntimeModeChange: (mode: RuntimeMode) => void; @@ -418,6 +419,7 @@ export function Composer({ onBranchChange, onNewTerminal, onAgentCommand, + commandResult, onModelChange, onModelSettingsChange, onRuntimeModeChange, @@ -503,14 +505,13 @@ export function Composer({ }); const skills = skillCatalog.skills; const [slashFilter, setSlashFilter] = useState(loadSlashFilter); - const [cliError, setCliError] = useState(""); - const cliSupported = !!onAgentCommand && supportsAgentCli(harness); - const effectiveFilter = cliSupported ? slashFilter : "all"; + const commandsSupported = !!onAgentCommand && supportsAgentCommands(harness); + const effectiveFilter = commandsSupported ? slashFilter : "all"; const slashItems = useMemo( () => [ PLAN_COMMAND, COMPACT_COMMAND, - ...(cliSupported ? agentCliCommands(harness) : []), + ...(commandsSupported ? agentCommands(harness) : []), ...skills.filter( (skill) => skill.kind === "native" || @@ -518,10 +519,10 @@ export function Composer({ skill.name !== COMPACT_COMMAND.name), ), ], - [skills, cliSupported, harness], + [skills, commandsSupported, harness], ); const skillLimit = - hasNativeCommands(harness) || cliSupported + hasNativeCommands(harness) || commandsSupported ? Number.POSITIVE_INFINITY : undefined; const rankedSkills = rankSkills( @@ -722,7 +723,11 @@ export function Composer({ const syncTokensFromTextarea = (el: HTMLTextAreaElement) => { if (creatingSkill) return; const cursor = el.selectionStart ?? 0; - const token = slashTokenAt(el.value, cursor, hasNativeCommands(harness)); + const token = slashTokenAt( + el.value, + cursor, + hasNativeCommands(harness) || commandsSupported, + ); setSlash(token); setMention(token ? null : mentionTokenAt(el.value, cursor)); }; @@ -921,29 +926,34 @@ export function Composer({ }, [addAttachments, attachmentsSupported, enabled]); const submit = (value: string) => { - const native = cliSupported - ? agentCliPrompt(value, harness, effectiveFilter, skills) + const native = commandsSupported + ? agentCommandPrompt(value, harness, effectiveFilter, skills) : null; - if (native) { - // Keep attachments/cards/draft intact when a native session cannot open. - if (attachments.length || inboxCard || noteCard || handoffCard) { - setCliError( - "Send or remove the attached items first, or use the CLI button to keep this draft in chat.", - ); + if (native && /^\/plan(?=\s|$)/.test(native)) { + value = native; + if (!consumePlanCommand(value).text.trim()) { + setPlanSelected(true); + if (ref.current) ref.current.value = ""; + setDraft(""); + onDraftChange?.(""); + setSlash(null); + syncHasValue("", attachments); return; } - if (!onAgentCommand?.(native)) return; - setCliError(""); + } else if (native) { + const accepted = onAgentCommand?.(native); + setSlash(null); + if (!accepted) return; if (ref.current) { ref.current.value = ""; ref.current.style.height = "auto"; } setDraft(""); onDraftChange?.(""); - setSlash(null); setMention(null); - setPlanSelected(false); - syncHasValue("", []); + // A chat command consumes only its text. Keep attachments, cards and + // the next-message plan selection in the same mounted composer. + syncHasValue("", attachments); return; } if (isCompactCommand(value)) { @@ -1030,13 +1040,22 @@ export function Composer({ e.key === "Enter" && !e.shiftKey && (isCompactCommand(e.currentTarget.value) || - (cliSupported && - agentCliPrompt( + (commandsSupported && + agentCommandPrompt( e.currentTarget.value, harness, effectiveFilter, skills, - ) !== null)) + ) !== null && + (!slash || + rankedSkills.length === 0 || + agentCommands(harness).some((item) => { + const name = e.currentTarget.value + .trim() + .split(/\s/)[0] + .replace(/^\/(agent:|cli:)?/, ""); + return item.name === name; + })))) ) { e.preventDefault(); submit(e.currentTarget.value); @@ -1125,12 +1144,8 @@ export function Composer({ onSteer={onSteerQueuedMessage} onResume={onResumeQueue} /> + {commandResult}
- {cliError ? ( -

- {cliError} -

- ) : null} {pickerOpen ? (
{ setSlashFilter(filter); saveSlashFilter(filter); @@ -1154,13 +1169,6 @@ export function Composer({ } : undefined } - onOpenCli={ - cliSupported - ? () => { - onAgentCommand?.(); - } - : undefined - } onStartCreate={() => { setCreatingSkill(true); setCreateError(null); @@ -1257,17 +1265,6 @@ export function Composer({ /> )}
- {cliSupported ? ( - - ) : null} void; filter?: SlashFilter; onFilterChange?: (filter: SlashFilter) => void; - onOpenCli?: () => void; }; export function SkillPicker({ @@ -44,7 +43,6 @@ export function SkillPicker({ onCreate, filter = "all", onFilterChange, - onOpenCli, }: Props) { return (
- {onOpenCli && filter !== "skills" ? ( - - ) : null} {filter !== "commands" ? ( -
- {draft ? ( -
{ - event.preventDefault(); - void send(); - }} - > - setDraft(event.target.value)} - className="min-w-0 flex-1 rounded bg-content/5 px-2 py-1 font-mono text-sm" - /> - - Finish any startup prompts below first. - - -
- ) : null} - {error ? ( -

- {error} -

- ) : null} -
- {command ? ( - { - sender.current = sendCommand; - setReady(!!sendCommand); - }} - onExit={() => setExited(true)} - /> - ) : ( -

- {error ? "CLI could not start." : "Starting CLI…"} -

- )} -
- - ); -} diff --git a/src/surfaces/SessionPane.tsx b/src/surfaces/SessionPane.tsx index 6f7daf6f..17443a02 100644 --- a/src/surfaces/SessionPane.tsx +++ b/src/surfaces/SessionPane.tsx @@ -42,8 +42,12 @@ import { loadNotesEnabled, subscribeNotesEnabled } from "../lib/settings"; import { resolveModel } from "../lib/models"; import { isAstraModel } from "../lib/astraWelcome"; import { AstraWelcome } from "./AstraWelcome"; -import { AgentCliView } from "./AgentCliView"; -import { supportsAgentCli, cliCommandInput } from "../lib/agentCli"; +import { AgentCommandPanel } from "../chrome/AgentCommandPanel"; +import { + runChatCommand, + supportsAgentCommands, + type CommandPanel, +} from "../lib/agentCommands"; type Props = { session: Session; @@ -240,34 +244,24 @@ export const SessionPane = memo(function SessionPane({ const showDeckProjectPicker = isEmpty && !looksLikeProject(session.cwd); const dockComposer = !isEmpty || inSplit || !!session.inboxAsk; const draftRef = useRef(undefined); - const [nativeCli, setNativeCli] = useState<{ command?: string } | null>(null); - const [nativeError, setNativeError] = useState(""); - const openAgentCli = (command?: string): boolean => { - if ( - session.busy || - session.queuedMessages?.length || - !supportsAgentCli(session.harness) - ) { - setNativeError( - "Finish the current turn and queued messages before opening the CLI.", - ); - return false; - } - try { - if (command) cliCommandInput(command); - setNativeError(""); - setNativeCli({ command }); - return true; - } catch (error) { - setNativeError(error instanceof Error ? error.message : String(error)); - return false; - } + const [commandPanel, setCommandPanel] = useState(null); + const handleAgentCommand = (command: string): boolean => { + const result = runChatCommand(session, command, { + onModelChange, + onModelSettingsChange, + onRuntimeModeChange, + onCompactContext, + onStop, + onOpenDiff, + }); + setCommandPanel(result.panel); + return result.accepted; }; const composer = ( onBranchChange(session.id)} onNewTerminal={() => onNewTerminal(session.id)} onAgentCommand={ - supportsAgentCli(session.harness) ? openAgentCli : undefined + supportsAgentCommands(session.harness) ? handleAgentCommand : undefined + } + commandResult={ + commandPanel && supportsAgentCommands(session.harness) ? ( + setCommandPanel(null)} + onModelChange={(model) => + onModelChange(session.id, session.harness, model) + } + onModelSettingsChange={(settings) => + onModelSettingsChange(session.id, settings) + } + onRuntimeModeChange={(mode) => + onRuntimeModeChange(session.id, mode) + } + /> + ) : null } onModelChange={(harness, model) => { onModelChange(session.id, harness, model); @@ -365,139 +377,120 @@ export const SessionPane = memo(function SessionPane({ className="relative isolate flex h-full min-h-0 min-w-0 flex-1 flex-col" onMouseDown={() => onFocus(session.id)} > - {nativeCli && supportsAgentCli(session.harness) ? ( - setNativeCli(null)} - /> + {astraWelcomeRun !== null && visible ? ( + ) : null} -
- {astraWelcomeRun !== null && visible ? ( - - ) : null} - {nativeError ? ( -

- {nativeError} -

- ) : null} - {inSplit ? ( -
{ - if (event.button !== 0 || !onPaneDragStart) return; - if ( - (event.target as HTMLElement | null)?.closest("[data-no-drag]") - ) { - return; - } - onPaneDragStart(event); + {inSplit ? ( +
{ + if (event.button !== 0 || !onPaneDragStart) return; + if ( + (event.target as HTMLElement | null)?.closest("[data-no-drag]") + ) { + return; + } + onPaneDragStart(event); + }} + > + {onPaneDragStart ? ( + + ) : null} + + + {title} + + +
+ ) : null} +
+ {isEmpty ? ( + session.inboxAsk ? ( +
+ +
+ ) : ( + + ) + ) : ( + <> + + onSecondOpinion(session.id, harness, turn, model) + : undefined + } + onHandoff={ + !session.inboxAsk && onHandoff + ? (harness, turn, model) => + onHandoff(session.id, harness, turn, model) + : undefined + } + onJumpToBottomChange={setShowJumpToBottom} + onJumpToBottomReady={onJumpToBottomReady} /> - - {title} - - -
- ) : null} -
- {isEmpty ? ( - session.inboxAsk ? ( -
- + {showJumpToBottom ? ( +
+
- ) : ( - - ) - ) : ( - <> - - onSecondOpinion(session.id, harness, turn, model) - : undefined - } - onHandoff={ - !session.inboxAsk && onHandoff - ? (harness, turn, model) => - onHandoff(session.id, harness, turn, model) - : undefined - } - onJumpToBottomChange={setShowJumpToBottom} - onJumpToBottomReady={onJumpToBottomReady} - /> - {showJumpToBottom ? ( -
- -
- ) : null} - - )} -
- {dockComposer ? ( -
{composer}
- ) : null} + ) : null} + + )}
+ {dockComposer ? ( +
{composer}
+ ) : null}
); }); diff --git a/src/surfaces/TerminalView.tsx b/src/surfaces/TerminalView.tsx index a196b67b..ef4e17fc 100644 --- a/src/surfaces/TerminalView.tsx +++ b/src/surfaces/TerminalView.tsx @@ -7,7 +7,6 @@ import { spawnPty, subscribePty, writePty, - type PtyCommand, } from "../lib/pty"; import { isOscColorQuery, oscColorReply } from "../lib/terminalChrome"; import { @@ -30,9 +29,6 @@ type Props = { cwd: string; active: boolean; onMetaChange?: (patch: TerminalMetaPatch) => void; - command?: PtyCommand; - onReady?: (send: ((text: string) => Promise) | null) => void; - onExit?: (code: number | null) => void; }; function cssColor(expr: string, fallback: string): string { @@ -112,15 +108,7 @@ function oscColors() { return isLightScheme() ? OSC_LIGHT : OSC_DARK; } -export function TerminalView({ - id, - cwd, - active, - onMetaChange, - command, - onReady, - onExit, -}: Props) { +export function TerminalView({ id, cwd, active, onMetaChange }: Props) { const outerRef = useRef(null); const hostRef = useRef(null); const termRef = useRef(null); @@ -129,10 +117,6 @@ export function TerminalView({ const onMetaChangeRef = useRef(onMetaChange); onMetaChangeRef.current = onMetaChange; const runningProcessRef = useRef(null); - const readyRef = useRef(onReady); - readyRef.current = onReady; - const exitRef = useRef(onExit); - exitRef.current = onExit; useEffect(() => { const outer = outerRef.current; @@ -206,34 +190,14 @@ export function TerminalView({ }, (code) => { if (closed) return; - spawned.current = false; - readyRef.current?.(null); - exitRef.current?.(code); const status = code == null ? "" : ` (${code})`; term.writeln(`\r\n[process exited${status}]`); }, ); - const starting = spawnPty(id, cwd, term.cols, term.rows, command) + const starting = spawnPty(id, cwd, term.cols, term.rows) .then(() => { - if (!closed) { - spawned.current = true; - readyRef.current?.(async (text) => { - if (closed || !spawned.current) - throw new Error("The CLI has exited."); - const input = term.modes.bracketedPasteMode - ? `\x1b[200~${text}\x1b[201~` - : text; - await writePty(id, input); - // Native TUIs coalesce bursts into paste events. Give their event - // loop time to accept the draft before delivering a separate Enter. - await new Promise((resolve) => setTimeout(resolve, 150)); - if (closed || !spawned.current) - throw new Error("The CLI has exited."); - await writePty(id, "\r"); - term.focus(); - }); - } + if (!closed) spawned.current = true; }) .catch((error) => { spawned.current = false; @@ -336,7 +300,6 @@ export function TerminalView({ return () => { closed = true; - readyRef.current?.(null); cancelAnimationFrame(frame); if (raf) cancelAnimationFrame(raf); observer.disconnect(); @@ -357,7 +320,7 @@ export function TerminalView({ termRef.current = null; spawned.current = false; }; - }, [id, cwd, command]); + }, [id, cwd]); // Identity-stable: the callers pass an inline arrow, so depending on the // prop itself would tear down and re-arm the poll — and re-fork `ps` — on diff --git a/tests/agent-cli.tsx b/tests/agent-cli.tsx deleted file mode 100644 index 5a856f48..00000000 --- a/tests/agent-cli.tsx +++ /dev/null @@ -1,100 +0,0 @@ -// Browser smoke fixture. IPC is mocked; it never starts a real agent or model turn. -import React, { useState } from "react"; -import { createRoot } from "react-dom/client"; -import { mockIPC, mockWindows } from "@tauri-apps/api/mocks"; -import { emit } from "@tauri-apps/api/event"; -import "../src/index.css"; -const calls: unknown[] = []; -Object.assign(window, { testCalls: calls }); -mockWindows("main"); -mockIPC( - (cmd, args) => { - calls.push({ cmd, args }); - if (cmd === "list_skills") - return [ - { - name: "research", - description: "Research skill", - path: "/repo/.agents/skills/research/SKILL.md", - scope: "project", - source: "agents", - }, - ]; - if (cmd === "home_dir" || cmd === "default_cwd") return "/repo"; - if (cmd.startsWith("harness_resolve_")) return { path: "/test/agent" }; - if ( - cmd === "list_project_files" || - cmd.includes("list_") || - cmd.includes("read_dir") - ) - return []; - if (cmd === "pty_spawn") { - setTimeout(() => { - void emit("pty-data", { - id: args?.id, - data: btoa("\x1b[?2004hAgent ready. Type / for commands.\r\n> "), - }); - }, 100); - return; - } - if (cmd === "pty_status") return { foreground: "agent" }; - return null; - }, - { shouldMockEvents: true }, -); -const { Composer } = await import("../src/chrome/Composer"); -const { AgentCliView } = await import("../src/surfaces/AgentCliView"); -const { newSession } = await import("../src/lib/session"); -function Fixture() { - const [harness, setHarness] = useState<"codex" | "claude">("codex"); - const [native, setNative] = useState<{ command?: string } | null>(null); - const [session] = useState(() => - newSession("codex", "/repo", "codex:gpt-6-astra"), - ); - return ( -
- - {native ? ( - setNative(null)} - /> - ) : null} -
- {}} - onCwdChange={() => {}} - onModelChange={() => {}} - onRuntimeModeChange={() => {}} - onSubmit={(text) => calls.push({ modelTurn: text })} - onAgentCommand={(command) => { - calls.push({ agentCommand: command ?? "" }); - setNative({ command }); - return true; - }} - /> -
-
- ); -} -createRoot(document.getElementById("root")!).render(); diff --git a/tests/agent-cli.html b/tests/agent-commands.html similarity index 51% rename from tests/agent-cli.html rename to tests/agent-commands.html index 9a40cc42..6a41ecfb 100644 --- a/tests/agent-cli.html +++ b/tests/agent-commands.html @@ -2,10 +2,10 @@ - MonoCode CLI test + MonoCode chat commands test
- + diff --git a/tests/agent-commands.tsx b/tests/agent-commands.tsx new file mode 100644 index 00000000..6d0fd3dc --- /dev/null +++ b/tests/agent-commands.tsx @@ -0,0 +1,167 @@ +// Browser regression fixture using the real SessionPane. IPC is mocked; +// commands cannot start a real agent, PTY or model turn here. +import React, { useState } from "react"; +import { createRoot } from "react-dom/client"; +import { mockIPC, mockWindows } from "@tauri-apps/api/mocks"; +import "../src/index.css"; +const calls: unknown[] = []; +Object.assign(window, { testCalls: calls }); +mockWindows("main"); +mockIPC( + (cmd, args) => { + calls.push({ cmd, args }); + if (cmd === "list_skills") + return [ + { + name: "research", + description: "Research skill", + path: "/repo/.agents/skills/research/SKILL.md", + scope: "project", + source: "agents", + }, + ]; + if (cmd === "home_dir" || cmd === "default_cwd") return "/repo"; + if (cmd === "git_status") + return { branch: "main", changes: [], isRepo: true }; + if (cmd.includes("list_") || cmd.includes("read_dir")) return []; + if (cmd.startsWith("harness_resolve_")) return { path: "/test/agent" }; + return null; + }, + { shouldMockEvents: true }, +); +const { SessionPane } = await import("../src/surfaces/SessionPane"); +const { newSession } = await import("../src/lib/session"); +const { setHarnessModels } = await import("../src/lib/models"); +for (const harness of ["codex", "claude"] as const) + setHarnessModels(harness, [ + { + id: `${harness}:current`, + harness, + name: `${harness} current`, + nativeId: "current", + settings: [ + { + id: "effort", + label: "Effort", + kind: "select", + value: "low", + options: [ + { value: "low", label: "Low" }, + { value: "high", label: "High" }, + ], + }, + { + id: "fast", + label: "Fast", + kind: "toggle", + value: "false", + options: [], + }, + ], + }, + { + id: `${harness}:next`, + harness, + name: `${harness} next`, + nativeId: "next", + }, + ]); +function Fixture() { + const [session, setSession] = useState(() => ({ + ...newSession("codex", "/repo", "codex:current"), + id: "existing-chat", + providerSessionId: "existing-provider-session", + context: { used: 32000, window: 128000 }, + blocks: [ + { + id: "user", + role: "user" as const, + text: "Keep this conversation in the chat UI.", + }, + { + id: "answer", + role: "assistant" as const, + text: "Existing conversation remains visible.", + }, + ], + })); + Object.assign(window, { testSession: session }); + return ( +
+ + {}} + onClose={() => {}} + onCwdChange={() => {}} + onBranchChange={() => {}} + onModelChange={(id, harness, model) => { + calls.push({ modelChange: { id, harness, model } }); + setSession((current) => ({ ...current, harness, model })); + }} + onModelSettingsChange={(id, modelSettings) => { + calls.push({ modelSettings: { id, modelSettings } }); + setSession((current) => ({ ...current, modelSettings })); + }} + onRuntimeModeChange={(id, runtimeMode) => { + calls.push({ permissionChange: { id, runtimeMode } }); + setSession((current) => ({ ...current, runtimeMode })); + }} + onSubmit={(id, text, attachments, options) => + calls.push({ modelTurn: { id, text, attachments, options } }) + } + onStop={(id) => { + calls.push({ stop: id }); + setSession((current) => ({ ...current, busy: false })); + }} + onCompactContext={(id) => { + calls.push({ compact: id }); + return true; + }} + onDeleteQueuedMessage={() => {}} + onEditQueuedMessage={() => {}} + onQueuedMessageEditingChange={() => {}} + onSteerQueuedMessage={() => {}} + onResumeQueue={() => {}} + onApproval={() => {}} + onQuestionReply={() => {}} + onOpenFile={() => {}} + onOpenDiff={(path, context) => calls.push({ diff: { path, context } })} + onOpenPlan={() => {}} + onBuildPlan={() => {}} + onNewTerminal={() => calls.push({ newTerminal: true })} + /> +
+ ); +} +createRoot(document.getElementById("root")!).render();