use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use a3s_code_core::config::OsConfig;
use a3s_code_core::context::RecentWorkspaceFilesContextProvider;
use a3s_code_core::hitl::TimeoutAction;
use a3s_code_core::workspace::{
LocalWorkspaceManifest, LocalWorkspaceManifestSnapshot, ManifestWorkspaceBackend,
WorkspaceServices,
};
use a3s_code_core::{
Agent, AgentEvent, AgentSession, SessionOptions, SystemPromptSlots, ToolCallResult,
};
use a3s_tui::cmd::{self, Cmd};
use a3s_tui::components::textarea::TextareaMsg;
use a3s_tui::components::viewport::ViewportMsg;
use a3s_tui::components::{
Alert, AlertKind, ChoicePrompt, ChoicePromptMsg, InlineAction, Scrollbar, SessionStatusChip,
Spinner, Textarea, Toast, ToastKind, ToolLogRecord as TuiToolLogRecord, ToolLogStatus,
Viewport,
};
use a3s_tui::event::{KeyEvent, MouseEvent};
use a3s_tui::keymap::{KeyBinding, Keymap};
use a3s_tui::layout::{Constraint, Layout};
use a3s_tui::style::{Color, Style};
use a3s_tui::{
AgentChrome, Event, KeyCode, KeyModifiers, Model, ProgramBuilder, Theme as TuiTheme,
};
use tokio::sync::{mpsc, Mutex};
use crate::top::{collect_processes, render_process_table, ProcessRow, ProcessTableView};
#[path = "assets/clone.rs"]
mod asset_clone;
#[path = "assets/lifecycle.rs"]
mod asset_lifecycle;
#[path = "assets/naming.rs"]
mod asset_naming;
#[path = "code_cli.rs"]
mod code_cli;
#[path = "code_serve.rs"]
mod code_serve;
pub(crate) use code_cli::{is_code_cli_command, run_code_cli};
#[path = "system/config.rs"]
mod config;
#[path = "system/skills.rs"]
pub(crate) mod skills;
#[path = "system/update.rs"]
mod update;
#[path = "workspace/gitutil.rs"]
mod gitutil;
#[path = "knowledge/kbutil.rs"]
mod kbutil;
#[path = "context/memutil.rs"]
mod memutil;
#[path = "os/progressive.rs"]
mod os_progressive;
#[path = "os/remote_ui.rs"]
mod remote_ui;
#[path = "os/runtime_policy.rs"]
mod runtime_policy;
mod runtime_projection;
#[path = "ui/design_markdown.rs"]
mod design_markdown;
#[path = "ui/image.rs"]
mod image;
#[path = "ui/render.rs"]
mod render;
#[path = "ui/syntax.rs"]
mod syntax;
#[path = "ui/util.rs"]
mod util;
mod panels;
use asset_naming::*;
use config::*;
use design_markdown::StreamingMarkdown;
use gitutil::*;
use image::*;
use memutil::*;
use render::*;
use runtime_policy::RuntimePolicy;
use runtime_projection::{RuntimeProjection, ToolCallRecord};
use skills::*;
use syntax::*;
use update::*;
use util::*;
const HITL_CONFIRM_TIMEOUT_MS: u64 = 60 * 60 * 1000;
const BACKGROUND_CONFIRM_TIMEOUT_MS: u64 = 500;
const TOOL_EXEC_TIMEOUT_MS: u64 = 30 * 60 * 1000;
const ACCENT: Color = Color::Rgb(0, 112, 243); const TN_GREEN: Color = ACCENT; const TN_YELLOW: Color = Color::Rgb(245, 166, 35); const TN_RED: Color = Color::Rgb(238, 0, 0); const TN_CYAN: Color = Color::Rgb(80, 227, 194); const TN_ORANGE: Color = Color::Rgb(249, 203, 40); const TN_PURPLE: Color = Color::Rgb(151, 71, 255); const TN_FG: Color = Color::Rgb(237, 237, 237); const TN_GRAY: Color = Color::Rgb(143, 143, 143); const SURFACE_SOFT: Color = Color::Rgb(31, 31, 31);
const SURFACE_SELECTED: Color = Color::Rgb(7, 49, 108);
const GRADIENT_DEVELOP_START: Color = Color::Rgb(0, 124, 240);
const GRADIENT_DEVELOP_END: Color = Color::Rgb(0, 223, 216);
const GRADIENT_PREVIEW_START: Color = TN_PURPLE;
const GRADIENT_PREVIEW_END: Color = Color::Rgb(255, 0, 128);
const GRADIENT_SHIP_START: Color = Color::Rgb(255, 77, 77);
const GRADIENT_SHIP_END: Color = Color::Rgb(249, 203, 40);
const BRAND_GRADIENT: [Color; 6] = [
GRADIENT_DEVELOP_START,
GRADIENT_DEVELOP_END,
GRADIENT_PREVIEW_START,
GRADIENT_PREVIEW_END,
GRADIENT_SHIP_START,
GRADIENT_SHIP_END,
];
fn agent_chrome_theme() -> TuiTheme {
TuiTheme {
primary: ACCENT,
secondary: TN_CYAN,
bg: Color::Black,
fg: TN_FG,
muted: TN_GRAY,
border: TN_GRAY,
success: TN_GREEN,
warning: TN_ORANGE,
error: TN_RED,
info: TN_CYAN,
surface: SURFACE_SOFT,
highlight: SURFACE_SELECTED,
}
}
fn agent_chrome(theme: &TuiTheme) -> AgentChrome<'_> {
AgentChrome::new(theme)
}
fn os_platform_guide(base_url: &str) -> String {
format!(
"[OS platform] You are signed in to the OS open platform at {base_url} (via /login). \
DEFAULT RULE: while signed in, \"OS\" in the user's questions ALWAYS means THIS OS platform — \
never this machine's operating system. So \"what's my OS account\", \"what modules does OS have\", \
etc. are about the platform. Answer them via the platform's progressive API; do NOT answer from \
this machine (whoami / hostname / paths / working directory describe the local box, not the \
platform — they are the WRONG answer). The endpoint and auth token are ALREADY in your shell \
environment (exported at login) — use them directly; do NOT read ~/.a3s/os-auth.json or any config \
file on each call:\n\
curl -s -X POST \"$A3S_OS_BASE_URL/api/v1/kernel/capabilities\" \
-H \"Authorization: Bearer $A3S_OS_TOKEN\" -H 'Content-Type: application/json' \
-d '{{\"action\":\"list\"}}'\n\
Body fields: `action` = list|search|describe|execute, plus `module` / `operation` / `params`. \
Go broad→narrow: `list` (modules) → `describe`/`search` for the one operation → `execute`. \
For `list`/`search`/`describe`, pipe through `jq` to extract only the fields you need so output \
stays a few lines (e.g. `| jq -r '.data.modules[].name'`). \
For `execute`, ALWAYS add `\"shaped\":true` to the request body — that is what makes the response \
carry the `.view` popup deep-link — and do NOT jq-narrow an execute response: pipe it whole (it is \
already compact), so `.view` survives. If you strip `.view` (or omit `\"shaped\":true`), the user \
loses the Open view button. \
Summarize the result for the user in a few lines; do NOT paste the whole raw JSON back. \
After your summary, ALWAYS output the trace on its own line, exactly \
`↳ requestId <requestId> · <timestamp>`. \
You do NOT print the view link yourself: whenever the execute output carries a `.view`, the host \
automatically shows a one-click `Open view` button that opens the authenticated progressive UI popup \
(the user's OS login is injected, no re-login). Never print the raw URL. The \
`a3s-os-capabilities` skill has full examples."
)
}
const SLASH_COMMANDS: &[(&str, &str)] = &[
(
"/model",
"switch model (←/→ for Claude/GPT accounts if signed in)",
),
("/init", "analyze the project and generate AGENTS.md"),
("/config", "edit config.acl in the built-in editor"),
("/theme", "cycle the code-highlight theme (Geist Dark …)"),
(
"/flow",
"select a workflow asset → OS Workflow as a Service designer (needs /login) · /flow <text> drafts one",
),
(
"/agent",
"select an agent definition → local dev · Agent as a Service or Function as a Service by kind",
),
(
"/mcp",
"select an MCP server asset → local dev · publish/debug/test via OS Function as a Service",
),
(
"/skill",
"select a skill asset → local dev · publish/deploy via OS Function as a Service",
),
(
"/okf",
"select an OKF package → local dev · publish/deploy via OS Knowledge service",
),
(
"/output",
"view every tool call this session (name · args · result)",
),
("/login", "sign in to the configured OS account"),
("/logout", "sign out from the configured OS account"),
("/view", "open the last OS view in a native window"),
("/plugin", "enable/disable Claude skills & plugins"),
("/reload", "re-scan skills/plugins (hot-reload the / menu)"),
("/update", "upgrade a3s to the latest release"),
("/btw", "ask a background side-question (/btw <prompt>)"),
("/top", "inspect local agent process activity"),
("/ide", "superfile-style file browser + editor"),
(
"/memory",
"browse memory as an event/entity graph with tiers and forget candidates",
),
(
"/kb",
"open the local personal knowledge base · add/import/search/vault",
),
(
"/ctx",
"search past sessions (ctx) · /ctx <n> attach · /ctx save <n> keep as memory",
),
("/effort", "adjust model effort (low … max)"),
("/compact", "summarize + compact the conversation context"),
("/goal", "set a north-star goal the agent keeps in mind"),
(
"/loop",
"engineered loop dashboard · agent-aware in /agent mode · /loop <task> quick loop",
),
(
"/sleep",
"consolidate today's work into memory (experience · preferences · knowledge)",
),
("/help", "show commands and shortcuts"),
(
"/fork",
"branch a new session from this point (original kept)",
),
("/clear", "reset the conversation"),
("/auto", "switch to auto-approve mode"),
("/exit", "quit a3s code"),
];
const IDLE_ONLY: &[&str] = &[
"/clear", "/compact", "/model", "/effort", "/goal", "/loop", "/reload", "/update", "/init",
"/fork", "/sleep", "/flow", "/agent", "/mcp", "/skill", "/okf", "/kb",
];
fn slash_candidates(input: &str) -> Vec<(&'static str, &'static str)> {
SLASH_COMMANDS
.iter()
.filter(|(cmd, _)| cmd.starts_with(input))
.copied()
.collect()
}
fn slash_tail<'a>(input: &'a str, command: &str) -> Option<&'a str> {
input
.strip_prefix(command)
.filter(|rest| rest.is_empty() || rest.starts_with(char::is_whitespace))
}
fn os_asset_category_query(category: &str, query: &str) -> String {
let query = query.trim();
if query.is_empty() {
format!("category:{category}")
} else {
format!("category:{category} {query}")
}
}
fn runtime_asset_query(category: &str, asset_hint: &str, query: &str) -> String {
let category = category.trim();
let asset_hint = asset_hint.trim();
let query = query.trim();
let mut parts = Vec::new();
if !category.is_empty() {
parts.push(format!("category:{category}"));
}
if !asset_hint.is_empty() {
parts.push(asset_hint.to_string());
}
if !query.is_empty() {
parts.push(query.to_string());
}
parts.join(" ")
}
fn cancel_pending_picker<Panel, Pending>(
picker: &mut Option<Panel>,
pending: &mut Option<Pending>,
) {
*picker = None;
*pending = None;
}
fn os_required_message(cmd: &str, os_configured: bool) -> String {
if os_configured {
format!(" {cmd} needs OS — sign in with /login first")
} else {
format!(" {cmd} needs OS — configure `os = \"https://your-os-host\"` in config.acl, then /login")
}
}
fn os_required_alert(cmd: &str, os_configured: bool) -> String {
let body = os_required_message(cmd, os_configured)
.trim_start()
.to_string();
format!(
" {}",
Alert::new(AlertKind::Warning, body).color(TN_YELLOW).view()
)
}
fn ide_flash_line(kind: ToastKind, message: impl Into<String>) -> String {
let color = match kind {
ToastKind::Info => TN_CYAN,
ToastKind::Success => TN_GREEN,
ToastKind::Warning => TN_YELLOW,
ToastKind::Error => TN_RED,
};
Toast::new(kind, message).color(color).view()
}
fn task_status_style(status: a3s_code_core::planning::TaskStatus) -> (char, Color) {
use a3s_code_core::planning::TaskStatus;
match status {
TaskStatus::Completed => ('✔', TN_GRAY),
TaskStatus::InProgress => ('▶', TN_YELLOW),
TaskStatus::Failed => ('✗', TN_RED),
TaskStatus::Skipped | TaskStatus::Cancelled => ('⊘', TN_GRAY),
_ => ('□', TN_GRAY), }
}
fn needs_synthesis(
synthesis_inflight: bool,
synthesis_used: bool,
had_agent_activity: bool,
text_after_activity: bool,
) -> bool {
!synthesis_inflight && !synthesis_used && had_agent_activity && !text_after_activity
}
fn estimate_tokens(s: &str) -> usize {
let (ascii, wide) = s.chars().fold((0usize, 0usize), |(a, w), c| {
if c.is_ascii() {
(a + 1, w)
} else {
(a, w + 1)
}
});
ascii / 4 + wide
}
const DEFAULT_CONTEXT_LIMIT: u32 = 128_000;
const CORE_MAX_CONTEXT_TOKENS: f32 = 200_000.0;
fn resolve_ctx_limit(raw: Option<u32>) -> u32 {
match raw {
Some(c) if c > 0 => c,
_ => DEFAULT_CONTEXT_LIMIT,
}
}
fn ctx_limit_for_model(model_ctx: &std::collections::HashMap<String, u32>, model: &str) -> u32 {
resolve_ctx_limit(
model_ctx
.get(model)
.copied()
.or_else(|| crate::codex::codex_model_context(model))
.or_else(|| inferred_ctx_limit(model)),
)
}
fn inferred_ctx_limit(model: &str) -> Option<u32> {
let m = model.trim().to_ascii_lowercase();
if let Some(limit) = context_suffix_limit(&m) {
return Some(limit);
}
if m.contains("claude") {
return Some(200_000);
}
if m.contains("gpt-5") || m.contains("gpt-4.1") {
return Some(1_000_000);
}
if m.contains("o1") || m.contains("o3") || m.contains("o4") {
return Some(200_000);
}
if m.contains("gpt-4o") || m.contains("gpt-4") || m.contains("glm") {
return Some(DEFAULT_CONTEXT_LIMIT);
}
None
}
fn context_suffix_limit(model: &str) -> Option<u32> {
if !model.ends_with(']') {
return None;
}
let start = model.rfind('[')?;
let suffix = model.get(start + 1..model.len().checked_sub(1)?)?;
if suffix.is_empty() {
return None;
}
let (number, scale) = suffix.split_at(suffix.len().saturating_sub(1));
let base = number.parse::<u32>().ok()?;
match scale {
"k" => base.checked_mul(1_000),
"m" => base.checked_mul(1_000_000),
_ => suffix.parse::<u32>().ok(),
}
}
fn auto_compact_threshold_for(window: u32) -> f32 {
let window = if window > 0 {
window as f32
} else {
CORE_MAX_CONTEXT_TOKENS
};
(0.85 * window / CORE_MAX_CONTEXT_TOKENS).clamp(0.01, 1.0)
}
fn ctx_warn_tier(pct: usize, warned: u8) -> (u8, Option<u8>) {
let tier: u8 = if pct >= 85 {
85
} else if pct >= 70 {
70
} else {
0
};
(tier, (tier > warned).then_some(tier))
}
fn workflow_doc_for_tool(name: &str, args: Option<&serde_json::Value>) -> Option<(String, String)> {
match name {
"dynamic_workflow" => {
let src = args
.and_then(|a| a.get("source"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())?;
Some((
format!("# Dynamic workflow script\n\n```javascript\n{src}\n```\n"),
"dynamic workflow script captured".to_string(),
))
}
"program" => {
let src = args
.and_then(|a| a.get("source"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())?;
Some((
format!("# Dynamic workflow script\n\n```javascript\n{src}\n```\n"),
"dynamic workflow script captured".to_string(),
))
}
"parallel_task" => {
let tasks = args
.and_then(|a| a.get("tasks"))
.and_then(|t| t.as_array())?
.iter()
.collect::<Vec<_>>();
workflow_doc_for_tasks(&tasks, true)
}
"task" => {
let args = args?;
if let Some(tasks) = args.get("tasks").and_then(|t| t.as_array()) {
let tasks = tasks.iter().collect::<Vec<_>>();
workflow_doc_for_tasks(&tasks, tasks.len() > 1)
} else {
workflow_doc_for_tasks(&[args], false)
}
}
_ => None,
}
}
fn workflow_doc_for_tasks(
tasks: &[&serde_json::Value],
parallel: bool,
) -> Option<(String, String)> {
if tasks.is_empty() {
return None;
}
let mut doc = if parallel {
format!(
"# Dynamic workflow\n\nFanned out {} parallel subagent task(s):\n\n",
tasks.len()
)
} else {
"# Dynamic workflow\n\nDelegated subagent task(s):\n\n".to_string()
};
for (i, task) in tasks.iter().enumerate() {
let desc = task
.get("description")
.or_else(|| task.get("prompt"))
.or_else(|| task.get("task"))
.and_then(|v| v.as_str())
.unwrap_or("(task)");
let agent = task
.get("agent")
.and_then(|v| v.as_str())
.unwrap_or("agent");
let prompt = task
.get("prompt")
.or_else(|| task.get("task"))
.and_then(|v| v.as_str())
.unwrap_or("");
doc.push_str(&format!(
"## {}. {desc}\n\nAgent: `{agent}`\n\n{prompt}\n\n",
i + 1
));
}
let label = if parallel {
format!("dynamic workflow · {} parallel tasks captured", tasks.len())
} else {
format!(
"dynamic workflow · {} delegated task{} captured",
tasks.len(),
if tasks.len() == 1 { "" } else { "s" }
)
};
Some((doc, label))
}
async fn fetch_top() -> Vec<ProcessRow> {
collect_processes().await.unwrap_or_default()
}
const TOP_HISTORY_LIMIT: usize = 32;
#[derive(Debug, Default)]
struct TopProcessHistory {
samples: HashMap<u32, TopMetricHistory>,
}
impl TopProcessHistory {
fn observe(&mut self, rows: &[ProcessRow]) {
let live = rows.iter().map(|row| row.pid).collect::<HashSet<_>>();
for row in rows {
self.samples
.entry(row.pid)
.or_default()
.push(row.cpu_pct, row.mem_pct);
}
self.samples.retain(|pid, _| live.contains(pid));
}
fn values(&self, pid: u32) -> (Vec<f32>, Vec<f32>) {
self.samples
.get(&pid)
.map(TopMetricHistory::values)
.unwrap_or_default()
}
fn clear(&mut self) {
self.samples.clear();
}
}
#[derive(Debug, Default)]
struct TopMetricHistory {
cpu: VecDeque<f32>,
mem: VecDeque<f32>,
}
impl TopMetricHistory {
fn push(&mut self, cpu: f32, mem: f32) {
push_top_history_value(&mut self.cpu, cpu);
push_top_history_value(&mut self.mem, mem);
}
fn values(&self) -> (Vec<f32>, Vec<f32>) {
(
self.cpu.iter().copied().collect(),
self.mem.iter().copied().collect(),
)
}
}
fn push_top_history_value(values: &mut VecDeque<f32>, value: f32) {
values.push_back(value);
while values.len() > TOP_HISTORY_LIMIT {
values.pop_front();
}
}
struct IdeEntry {
path: std::path::PathBuf,
name: String,
depth: usize,
is_dir: bool,
expanded: bool,
}
#[derive(Clone, PartialEq, Eq, Debug)]
enum IdePrompt {
Search { forward: bool, text: String },
Command(String),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum EditMode {
Normal,
Insert,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct PendingOp {
op: char,
count: usize,
}
#[derive(Clone, PartialEq, Eq, Debug)]
enum RepeatEdit {
DeleteChar(usize),
DeleteLine(usize),
DeleteWord(usize),
DeleteToEol,
ChangeLine(usize),
Replace(char),
JoinLine(usize),
ToggleCase(usize),
}
struct IdeFile {
path: std::path::PathBuf,
lines: Vec<String>, scroll: usize, hscroll: usize, row: usize, col: usize, dirty: bool,
image: bool, readonly: bool, mode: EditMode, pending: Option<PendingOp>,
count: Option<usize>,
undo: Vec<(Vec<String>, usize, usize)>,
redo: Vec<(Vec<String>, usize, usize)>,
last_change: Option<RepeatEdit>,
search: Option<(String, bool)>,
visual_line_anchor: Option<usize>,
clip: String, clip_linewise: bool, }
impl IdeFile {
fn new(path: std::path::PathBuf, lines: Vec<String>, image: bool, readonly: bool) -> Self {
IdeFile {
path,
lines: if lines.is_empty() {
vec![String::new()]
} else {
lines
},
scroll: 0,
hscroll: 0,
row: 0,
col: 0,
dirty: false,
image,
readonly,
mode: EditMode::Normal,
pending: None,
count: None,
undo: Vec::new(),
redo: Vec::new(),
last_change: None,
search: None,
visual_line_anchor: None,
clip: String::new(),
clip_linewise: false,
}
}
}
fn format_tool_log_records(records: &[ToolCallRecord], width: usize) -> Option<String> {
if records.is_empty() {
return None;
}
const OUTPUT_TAIL: usize = 24;
let records = records
.iter()
.map(|rec| {
let arg = rec
.args
.as_ref()
.and_then(|args| arg_summary_for_tool(&rec.name, args))
.unwrap_or_default();
let action = if arg.is_empty() {
tool_verb(&rec.name).to_string()
} else {
format!("{} {}", tool_verb(&rec.name), arg)
};
let status = if rec.exit_code == 0 {
ToolLogStatus::Ok
} else {
ToolLogStatus::Exit(rec.exit_code)
};
let args = rec
.args
.as_ref()
.map(serde_json::to_string)
.transpose()
.unwrap_or_default();
let mut record = TuiToolLogRecord::new(action, status).output(rec.output.clone());
if let Some(args) = args {
record = record.args(args);
}
record
})
.collect::<Vec<_>>();
let theme = agent_chrome_theme();
let chrome = agent_chrome(&theme);
Some(
chrome
.tool_log()
.records(records)
.max_output_lines_per_record(OUTPUT_TAIL)
.view(width as u16, usize::MAX),
)
}
fn deep_research_workflow_source() -> &'static str {
r#"
async function run(ctx, inputs) {
const input = inputs.input || {};
const query = input.query || "";
const tracks = Array.isArray(input.tracks) && input.tracks.length > 0
? input.tracks
: [
{ title: "Facts and timeline", focus: "establish the current facts, dates, and key actors" },
{ title: "Primary sources", focus: "find official or primary-source evidence" },
{ title: "Independent analysis", focus: "compare reputable independent analysis and disagreements" },
{ title: "Risks and caveats", focus: "identify uncertainty, recency caveats, and weak claims" },
];
const localTasks = () => tracks.map((track, index) => {
const title = track.title || `Track ${index + 1}`;
const focus = track.focus || String(track);
return {
agent: "general",
description: `Research ${index + 1}: ${title}`,
prompt: `Deep-research track for: ${query}\n\nFocus: ${focus}\n\nUse web_search and web_fetch. Return URLs, publication dates, key evidence, contradictions, and confidence notes.`
};
});
if (inputs.kind === "workflow") {
const runtimeResearch = inputs.step_outputs.runtime_research;
const localResearch = inputs.step_outputs.local_research;
const localFallback = inputs.step_outputs.local_fallback;
if (localFallback) {
return {
type: "complete",
output: {
query,
mode: "local_fallback",
runtime_error: runtimeResearch && runtimeResearch.runtime_error,
research: localFallback
}
};
}
if (localResearch) {
return { type: "complete", output: { query, mode: "local_parallel_task", research: localResearch } };
}
if (runtimeResearch && !runtimeResearch.runtime_error) {
return { type: "complete", output: { query, mode: "os_runtime", research: runtimeResearch } };
}
if (runtimeResearch && runtimeResearch.runtime_error) {
return {
type: "schedule_step",
step_id: "local_fallback",
step_name: "parallel_task",
input: { tasks: localTasks() },
retry: { max_attempts: 1, delay_ms: 0 },
};
}
if (input.os_runtime) {
return {
type: "schedule_step",
step_id: "runtime_research",
step_name: "runtime_research",
input: {
query,
worker: input.worker || "deep-research-worker",
tracks,
},
retry: { max_attempts: 1, delay_ms: 0 },
};
}
return {
type: "schedule_step",
step_id: "local_research",
step_name: "parallel_task",
input: { tasks: localTasks() },
retry: { max_attempts: 1, delay_ms: 0 },
};
}
if (inputs.kind === "step" && inputs.step_name === "runtime_research") {
const result = await ctx.tool("runtime", {
worker: inputs.input.worker,
timeout_ms: 600000,
tasks: inputs.input.tracks.map((track, index) => ({
query: inputs.input.query,
title: track.title || `Track ${index + 1}`,
focus: track.focus || String(track),
requirements: "Use web search and full-page reads. Return URLs, dates, key evidence, contradictions, and confidence notes."
}))
});
if (!result || result.exitCode !== 0) {
return {
runtime_error: (result && result.output) || "runtime tool failed",
runtime_result: result || null
};
}
return { mode: "os_runtime", runtime: result };
}
return { error: `unknown dynamic workflow invocation: ${inputs.kind}/${inputs.step_name || ""}` };
}
"#
}
#[cfg(test)]
fn deep_research_prompt(query: &str, os_runtime: bool) -> String {
let tracks_directive = if os_runtime {
"This query is broad enough for OS Runtime fan-out. Set \
`os_runtime: true` in the workflow input and include \
`allowed_tools: [\"runtime\"]`. Split the query into 4-8 independent \
research tracks before the call. After merging the tracks, create both a \
Markdown report and a standalone HTML page. If an OS progressive \
operation naturally returns `.view` or `viewUrl`, preserve that shaped \
response so the TUI can show a one-click view; otherwise provide the \
local report artifacts and do not fabricate RemoteUI evidence."
.to_string()
} else {
"OS Runtime is unavailable or not warranted for this query. Set `os_runtime: false`; \
the DynamicWorkflowRuntime script will schedule a host-side \
`parallel_task` step for local subagent fan-out because PTC itself \
cannot call `parallel_task`. Still create a Markdown report and \
standalone HTML page under `.a3s/research/<slug>/`, and tell the user \
the local paths. Do not claim a RemoteUI view was created without an OS \
view response."
.to_string()
};
let source = deep_research_workflow_source();
format!(
"Conduct deep research to answer the query below. Be thorough.\n\n\
Required execution path:\n\
1. First call `dynamic_workflow` with the JavaScript source below. \
The workflow must gather evidence through Flow before final synthesis.\n\
2. Provide `input.query` and 4-8 `input.tracks` with `title` and `focus`. \
{tracks_directive}\n\
3. After `dynamic_workflow` returns, read the evidence, cross-check \
claims across independent sources, call out disagreements and recency \
caveats, then synthesize a comprehensive answer with inline citations.\n\
4. Produce a final \"Sources\" list of URLs used and clear links to \
report artifacts/view.\n\n\
Dynamic workflow source:\n\
```javascript\n{source}\n```\n\n\
Query: {query}"
)
}
fn deep_research_workflow_args(query: &str, os_runtime: bool) -> serde_json::Value {
let allowed_tools = if os_runtime {
serde_json::json!(["runtime"])
} else {
serde_json::json!([])
};
serde_json::json!({
"source": deep_research_workflow_source(),
"input": {
"query": query,
"os_runtime": os_runtime,
},
"allowed_tools": allowed_tools,
"limits": {
"timeoutMs": 900000,
"maxToolCalls": 8,
"maxOutputBytes": 262144
}
})
}
fn should_use_os_runtime_for_deep_research(query: &str, os_available: bool) -> bool {
if !os_available {
return false;
}
let q = query.trim().to_lowercase();
if q.is_empty() {
return false;
}
let local_only_markers = [
"local only",
"locally",
"no os",
"without os",
"不要 os",
"不用 os",
"不使用 os",
"本地",
"不要远程",
"不用远程",
];
if local_only_markers.iter().any(|marker| q.contains(marker)) {
return false;
}
let broad_markers = [
"comprehensive",
"deep dive",
"landscape",
"benchmark",
"compare",
"comparison",
"market",
"regulation",
"policy",
"paper",
"papers",
"latest",
"recent",
"timeline",
"多来源",
"全面",
"深入",
"调研",
"研究",
"对比",
"比较",
"竞品",
"市场",
"法规",
"政策",
"论文",
"最新",
"趋势",
"时间线",
"大量",
"并行",
];
broad_markers.iter().any(|marker| q.contains(marker))
|| q.split_whitespace().count() >= 14
|| q.chars().count() >= 80
}
fn deep_research_synthesis_prompt(
query: &str,
os_runtime: bool,
workflow_output: &str,
workflow_metadata: Option<&serde_json::Value>,
) -> String {
let remoteui_directive = if os_runtime {
"OS Runtime was selected for this run because the query looked broad or \
highly parallelizable. If the gathered evidence already includes a \
shaped `.view` or `viewUrl`, preserve it so the TUI can surface the \
view. Otherwise provide Markdown/HTML report artifacts and do not claim \
RemoteUI success."
.to_string()
} else {
"OS Runtime was not selected for this run. Create a Markdown report and \
standalone HTML page under `.a3s/research/<slug>/`, provide local paths, \
and do not claim a RemoteUI view unless a later shaped response exists."
.to_string()
};
let metadata = workflow_metadata
.and_then(|metadata| serde_json::to_string_pretty(metadata).ok())
.unwrap_or_else(|| "{}".to_string());
format!(
"Synthesize the deep-research answer for the query below.\n\n\
A host-controlled DynamicWorkflowRuntime run has already gathered the \
research evidence. Do not call `dynamic_workflow` again. Use the workflow \
evidence below, cross-check claims, call out disagreements and recency \
caveats, and write a comprehensive answer with inline citations and a \
final Sources list. If the workflow output reports a `runtime_error` and \
`mode: \"local_fallback\"`, clearly mention that OS Runtime was attempted \
and local parallel research was used as the fallback.\n\n\
{remoteui_directive}\n\n\
Query:\n{query}\n\n\
DynamicWorkflowRuntime output:\n```json\n{workflow_output}\n```\n\n\
DynamicWorkflowRuntime metadata:\n```json\n{metadata}\n```"
)
}
fn deep_research_recovery_prompt(
query: &str,
os_runtime: bool,
workflow_error: &str,
workflow_metadata: Option<&serde_json::Value>,
) -> String {
let recovery_path = if os_runtime {
"The host-controlled workflow selected OS Runtime and failed before usable \
evidence was gathered. Recover with local web_search/web_fetch or the \
signed-in `runtime` tool only if it is clearly available and useful; if \
the runtime worker or endpoint is unavailable, explain the failure and \
continue locally."
.to_string()
} else {
"OS Runtime was not selected. Recover with local web_search/web_fetch and \
local artifacts under `.a3s/research/<slug>/`."
.to_string()
};
let metadata = workflow_metadata
.and_then(|metadata| serde_json::to_string_pretty(metadata).ok())
.unwrap_or_else(|| "{}".to_string());
format!(
"Recover and complete the deep-research task for the query below.\n\n\
The host-controlled DynamicWorkflowRuntime preflight failed. Do not call \
`dynamic_workflow` again in this recovery turn. {recovery_path}\n\n\
Query:\n{query}\n\n\
DynamicWorkflowRuntime error:\n```text\n{workflow_error}\n```\n\n\
DynamicWorkflowRuntime metadata:\n```json\n{metadata}\n```\n\n\
Deliver a comprehensive answer with inline citations, a final Sources \
list, and report artifacts/view as appropriate."
)
}
fn json_contains_tool_evidence(value: &serde_json::Value, tool: &str) -> bool {
match value {
serde_json::Value::Object(map) => map.iter().any(|(key, value)| {
((key == "name" || key == "tool" || key == "tool_name") && value.as_str() == Some(tool))
|| json_contains_tool_evidence(value, tool)
}),
serde_json::Value::Array(items) => items
.iter()
.any(|item| json_contains_tool_evidence(item, tool)),
_ => false,
}
}
fn deep_research_goal(query: &str) -> String {
format!("Deep research — deliver a comprehensive, well-cited report answering: {query}")
}
fn append_scrollbar(view: &str, inner_width: usize, total: usize, scroll_percent: u8) -> String {
let visible = view.split('\n').count();
Scrollbar::from_scroll_percent(total, visible, scroll_percent)
.track_color(TN_GRAY)
.thumb_color(ACCENT)
.hide_when_not_overflowing(true)
.append_to_view(view, inner_width)
}
fn osc52_copy(text: &str) -> String {
use base64::Engine;
let capped: String = text.chars().take(64_000).collect();
let b64 = base64::engine::general_purpose::STANDARD.encode(capped.as_bytes());
format!("\x1b]52;c;{b64}\x07")
}
const VIEW_BUTTON_MARKER: &str = "Open view";
const VIEW_BUTTON_CLICK_DRIFT_COLS: u16 = 2;
fn remote_view_button(detail: &str) -> String {
InlineAction::new(VIEW_BUTTON_MARKER)
.icon("↗")
.colors(Color::BrightWhite, ACCENT)
.detail_color(TN_GRAY)
.detail(detail)
.view()
}
fn copy_to_clipboard(text: &str) {
use std::io::Write;
let mut out = std::io::stdout();
let _ = out.write_all(osc52_copy(text).as_bytes());
let _ = out.flush();
#[cfg(target_os = "macos")]
{
if let Ok(mut child) = std::process::Command::new("pbcopy")
.stdin(std::process::Stdio::piped())
.spawn()
{
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(text.as_bytes());
}
let _ = child.wait();
}
}
}
const SELECTION_BG: Color = SURFACE_SELECTED;
#[derive(Clone, Copy)]
struct Selection {
anchor: (u16, u16),
head: (u16, u16),
}
impl Selection {
fn is_empty(&self) -> bool {
self.anchor == self.head
}
fn ordered(&self) -> (usize, usize, usize, usize) {
let (a, b) = if self.anchor <= self.head {
(self.anchor, self.head)
} else {
(self.head, self.anchor)
};
(a.0 as usize, a.1 as usize, b.0 as usize, b.1 as usize)
}
}
fn viewport_mouse_cell(
row: u16,
column: u16,
viewport_rows: usize,
max_col: u16,
) -> Option<(u16, u16)> {
if viewport_rows == 0 || row as usize >= viewport_rows {
None
} else {
Some((row, column.min(max_col)))
}
}
fn viewport_mouse_cell_clamped(
row: u16,
column: u16,
viewport_rows: usize,
max_col: u16,
) -> Option<(u16, u16)> {
if viewport_rows == 0 {
None
} else {
Some((
row.min(viewport_rows.saturating_sub(1) as u16),
column.min(max_col),
))
}
}
fn slice_cols(s: &str, from: usize, to: usize) -> String {
let mut col = 0usize;
let mut out = String::new();
for ch in s.chars() {
if col >= to {
break;
}
if col >= from {
out.push(ch);
}
col += a3s_tui::style::visible_len(&ch.to_string());
}
out
}
fn selection_to_text(view: &str, r1: usize, c1: usize, r2: usize, c2: usize) -> String {
let rows: Vec<&str> = view.split('\n').collect();
let mut out: Vec<String> = Vec::new();
for r in r1..=r2 {
let Some(row) = rows.get(r) else { break };
let plain = a3s_tui::style::strip_ansi(row);
let from = if r == r1 { c1 } else { 0 };
let to = if r == r2 { c2 } else { usize::MAX };
out.push(slice_cols(&plain, from, to).trim_end().to_string());
}
out.join("\n")
}
fn viewport_row_contains_view_button(view: &str, row: u16) -> bool {
view.split('\n')
.nth(row as usize)
.map(a3s_tui::style::strip_ansi)
.is_some_and(|line| line.to_ascii_lowercase().contains("open view"))
}
fn is_remote_view_click(view: &str, selection: Selection) -> bool {
selection.anchor.0 == selection.head.0
&& selection.anchor.1.abs_diff(selection.head.1) <= VIEW_BUTTON_CLICK_DRIFT_COLS
&& viewport_row_contains_view_button(view, selection.anchor.0)
}
fn is_quit_key(key: &KeyEvent) -> bool {
key.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(key.code, KeyCode::Char(c) if c.eq_ignore_ascii_case(&'c'))
}
fn quit_is_confirmed(armed: Option<Instant>, now: Instant) -> bool {
armed.is_some_and(|t| now.saturating_duration_since(t) < Duration::from_secs(2))
}
fn highlight_selection(view: &str, r1: usize, c1: usize, r2: usize, c2: usize) -> String {
let bg = Style::new().bg(SELECTION_BG).fg(TN_FG);
view.split('\n')
.enumerate()
.map(|(i, row)| {
if i < r1 || i > r2 {
return row.to_string();
}
let plain = a3s_tui::style::strip_ansi(row);
let from = if i == r1 { c1 } else { 0 };
let to = if i == r2 { c2 } else { usize::MAX };
let before = slice_cols(&plain, 0, from);
let sel = slice_cols(&plain, from, to);
let after = slice_cols(&plain, to, usize::MAX);
format!("{before}{}{after}", bg.render(&sel))
})
.collect::<Vec<_>>()
.join("\n")
}
struct Ide {
entries: Vec<IdeEntry>,
sel: usize,
tree_scroll: usize,
file: Option<IdeFile>,
focus_editor: bool,
flash: Option<String>,
title: String,
preview: Option<(std::path::PathBuf, Vec<String>)>,
prompt: Option<IdePrompt>,
kb_root: Option<std::path::PathBuf>,
armed_delete: Option<std::path::PathBuf>,
delete_confirm_yes: bool,
}
impl Ide {
fn browse(entries: Vec<IdeEntry>, title: &str) -> Self {
Ide {
entries,
sel: 0,
tree_scroll: 0,
file: None,
focus_editor: false,
flash: None,
title: title.to_string(),
preview: None,
prompt: None,
kb_root: None,
armed_delete: None,
delete_confirm_yes: true,
}
}
}
fn ide_children(dir: &std::path::Path, depth: usize) -> Vec<IdeEntry> {
let mut v: Vec<IdeEntry> = std::fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.filter_map(|e| {
let name = e.file_name().to_string_lossy().into_owned();
if matches!(
name.as_str(),
".git" | "node_modules" | "target" | ".DS_Store" | ".next" | "dist"
) {
return None;
}
let is_dir = e.path().is_dir();
Some(IdeEntry {
path: e.path(),
name,
depth,
is_dir,
expanded: false,
})
})
.collect();
v.sort_by(|a, b| {
b.is_dir
.cmp(&a.is_dir)
.then(a.name.to_lowercase().cmp(&b.name.to_lowercase()))
});
v
}
fn project_instructions(workspace: &str) -> Option<String> {
for name in ["CLAUDE.md", "AGENT.md"] {
let p = std::path::Path::new(workspace).join(name);
if let Ok(c) = std::fs::read_to_string(&p) {
if !c.trim().is_empty() {
return Some(format!("# Project Instructions ({name})\n\n{c}"));
}
}
}
None
}
const PAD: usize = 2;
fn viewport_content_width_for(width: u16) -> usize {
(width as usize).saturating_sub(1)
}
fn transcript_markdown_width_for(width: u16) -> usize {
viewport_content_width_for(width).saturating_sub(PAD + 2)
}
struct EffortProfile {
label: &'static str,
thinking_budget: usize,
max_tool_rounds: usize,
max_continuation_turns: u32,
guideline: Option<&'static str>,
}
const EFFORT_LEVELS: &[EffortProfile] = &[
EffortProfile {
label: "low",
thinking_budget: 1024,
max_tool_rounds: 120,
max_continuation_turns: 2,
guideline: Some(EFFORT_LOW),
},
EffortProfile {
label: "medium",
thinking_budget: 4096,
max_tool_rounds: 200,
max_continuation_turns: 3,
guideline: None,
},
EffortProfile {
label: "high",
thinking_budget: 8192,
max_tool_rounds: 300,
max_continuation_turns: 4,
guideline: Some(EFFORT_HIGH),
},
EffortProfile {
label: "xhigh",
thinking_budget: 16384,
max_tool_rounds: 400,
max_continuation_turns: 6,
guideline: Some(EFFORT_XHIGH),
},
EffortProfile {
label: "max",
thinking_budget: 32768,
max_tool_rounds: 500,
max_continuation_turns: 8,
guideline: Some(EFFORT_MAX),
},
EffortProfile {
label: "ultracode",
thinking_budget: 32768,
max_tool_rounds: 600,
max_continuation_turns: 8,
guideline: Some(ULTRACODE_GUIDELINES),
},
];
const ULTRACODE: usize = 5;
const EFFORT_LOW: &str = "\
[effort: low] Favor speed and minimalism. Answer directly, make the smallest \
change that works (reading enough surrounding code to change it safely), and \
keep verification proportionate: still run the narrowest build/test/type-check \
that covers what you touched — just don't add checks or scope the task didn't \
warrant. Don't gold-plate.";
const EFFORT_HIGH: &str = "\
[effort: high] Favor depth. Reason through the approach before acting. After \
changes, verify the narrow path you touched (build / test / type-check) and \
check the obvious edge cases, then re-read your own diff for correctness before \
finishing.";
const EFFORT_XHIGH: &str = "\
[effort: xhigh] Work rigorously. Before choosing an approach, weigh at \
least one alternative. Verify thoroughly — run the relevant tests/build, probe \
edge cases and failure modes, and confirm the change actually does what was \
asked. Do a self-review pass for correctness and simplicity before concluding.";
const EFFORT_MAX: &str = "\
[effort: max] Maximum rigor; prefer correctness and completeness over speed. \
Decompose the problem, compare alternatives, and implement the strongest \
solution. Verify exhaustively: tests, build, edge cases, and boundary / \
adversarial inputs. Finish with a self-critique pass that actively hunts for \
what you may have missed or gotten wrong, and fix it before concluding.";
const ULTRACODE_GUIDELINES: &str = "\
[ultracode] Dynamic-workflow mode is available — you decide whether a turn needs \
it. Match the effort to the task: answer trivial or conversational input (a \
greeting, a single question, a one-step edit) directly, with no plan and no \
fan-out. When a task genuinely needs a dynamic workflow, call the \
`dynamic_workflow` tool with one sandboxed JavaScript PTC workflow script. In \
that script, return A3S Flow commands for workflow replay. Use PTC `ctx` tools \
inside ordinary steps (`ctx.read`, `ctx.grep`, `ctx.tool(\"runtime\", ...)` when \
the login-gated runtime tool exists). For local parallel subagent fan-out, \
schedule a Flow step with `step_name: \"parallel_task\"`; do not call \
`parallel_task` from PTC. Keep child prompts bounded and evidence-oriented, then \
synthesize results before completing the workflow.";
#[derive(Clone, Copy, PartialEq)]
enum Mode {
Default,
Plan,
Auto,
}
impl Mode {
fn next(self) -> Self {
match self {
Mode::Default => Mode::Plan,
Mode::Plan => Mode::Auto,
Mode::Auto => Mode::Default,
}
}
fn glyph(self) -> &'static str {
match self {
Mode::Default => "⏵",
Mode::Plan => "✎",
Mode::Auto => "⏵⏵",
}
}
fn name(self) -> &'static str {
match self {
Mode::Default => "default",
Mode::Plan => "plan",
Mode::Auto => "auto",
}
}
fn color(self) -> Color {
match self {
Mode::Default => TN_FG,
Mode::Plan => TN_CYAN,
Mode::Auto => TN_GREEN,
}
}
fn auto_approves(self, tool: &str) -> bool {
match self {
Mode::Auto => true,
Mode::Plan => is_readonly_tool(tool),
Mode::Default => false,
}
}
}
fn is_readonly_tool(name: &str) -> bool {
matches!(
name,
"read" | "grep" | "ls" | "glob" | "find" | "search" | "web_search" | "web_fetch"
)
}
struct Queued {
prio: u8,
seq: u64,
text: String,
display: String,
runtime_expectation: Option<RuntimeExpectation>,
deep_research: Option<(String, bool)>,
}
impl PartialEq for Queued {
fn eq(&self, o: &Self) -> bool {
self.prio == o.prio && self.seq == o.seq
}
}
impl Eq for Queued {}
impl Ord for Queued {
fn cmp(&self, o: &Self) -> std::cmp::Ordering {
o.prio.cmp(&self.prio).then(o.seq.cmp(&self.seq))
}
}
impl PartialOrd for Queued {
fn partial_cmp(&self, o: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(o))
}
}
type SharedRx = Arc<Mutex<mpsc::Receiver<AgentEvent>>>;
type SharedManifestRx =
Arc<Mutex<tokio::sync::broadcast::Receiver<LocalWorkspaceManifestSnapshot>>>;
type StreamJoin = tokio::task::JoinHandle<()>;
#[derive(PartialEq)]
enum State {
Idle,
Streaming,
Awaiting,
}
#[derive(Clone)]
#[allow(clippy::enum_variant_names)]
enum Action {
ScrollUp,
ScrollDown,
ScrollTop,
ScrollBottom,
}
static UPGRADE_ON_EXIT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
static LATEST: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
enum Msg {
Term(Event),
Agent(Box<AgentEvent>),
Submit(String),
StreamStarted(SharedRx, StreamJoin),
StreamEnded,
StreamError(String),
WorkspaceManifest(Box<LocalWorkspaceManifestSnapshot>),
WorkspaceManifestStopped,
SpinnerTick,
BannerTick,
ModalConfirm(usize),
Resume,
Interrupted,
ShellOutput(String),
DeepResearchWorkflowCompleted {
query: String,
os_runtime: bool,
args: serde_json::Value,
result: Result<ToolCallResult, String>,
},
UpdatePlan(Option<String>),
UpdateRepair(Result<Vec<String>, String>),
OsLogin(Result<String, String>),
SshKeySynced(crate::a3s_os::SshKeyOutcome),
OsRefreshed(Result<crate::a3s_os::StoredOsSession, String>),
OsGatewayModels(Result<Vec<crate::a3s_os::GatewayModel>, String>),
SideNote(String),
TopData(Vec<ProcessRow>),
TopRefresh,
Forked(Result<String, String>),
MemoryLoaded(MemPanelData),
MemoryForgotten(Result<(String, MemPanelData), String>),
AssetListLoaded(Result<panels::asset_resources::AssetListFetch, String>),
RuntimeActivityLoaded(Result<panels::asset_resources::RuntimeActivityFetch, String>),
KbAdded(String),
CtxResults(Result<String, String>),
CtxWindow(Result<(String, String), String>),
CtxSaved(Result<String, String>),
SleepSaved(Result<usize, String>),
FlowOsCompleted(Result<panels::flow::FlowOsResult, String>),
AgentOsCompleted(Result<panels::agent::AgentOsResult, String>),
McpOsCompleted(Result<panels::mcp::McpOsResult, String>),
SkillOsCompleted(Result<panels::skill::SkillOsResult, String>),
OkfOsCompleted(Result<panels::okf::OkfOsResult, String>),
AssetCloned(Result<asset_clone::AssetCloneResult, String>),
CtxMemorySource(Result<(String, String), String>),
AutoReview(String),
Compacted(String),
UpdateCheck(Option<String>),
}
impl From<Event> for Msg {
fn from(event: Event) -> Self {
Msg::Term(event)
}
}
fn pump(rx: SharedRx) -> Cmd<Msg> {
cmd::cmd(move || async move {
let mut guard = rx.lock().await;
match guard.recv().await {
Some(event) => Msg::Agent(Box::new(event)),
None => Msg::StreamEnded,
}
})
}
fn pump_manifest(rx: SharedManifestRx) -> Cmd<Msg> {
cmd::cmd(move || async move {
let mut guard = rx.lock().await;
loop {
match guard.recv().await {
Ok(snapshot) => return Msg::WorkspaceManifest(Box::new(snapshot)),
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
return Msg::WorkspaceManifestStopped;
}
}
}
})
}
fn spinner_tick() -> Cmd<Msg> {
cmd::tick(Duration::from_millis(80), Msg::SpinnerTick)
}
fn resume_after_pending_confirmation_cmd(rx: Option<SharedRx>) -> Cmd<Msg> {
let mut cmds = vec![spinner_tick()];
if let Some(rx) = rx {
cmds.push(pump(rx));
}
cmd::batch(cmds)
}
fn banner_tick() -> Cmd<Msg> {
cmd::tick(Duration::from_millis(280), Msg::BannerTick)
}
fn with_recent_workspace_context(
opts: SessionOptions,
manifest: &Arc<LocalWorkspaceManifest>,
) -> SessionOptions {
opts.with_context_provider(Arc::new(RecentWorkspaceFilesContextProvider::new(
manifest.clone(),
)))
}
fn tui_session_options(confirmation: a3s_code_core::hitl::ConfirmationPolicy) -> SessionOptions {
SessionOptions::new()
.with_confirmation_policy(confirmation)
.with_permission_policy(tui_permission_policy())
.with_tool_timeout(TOOL_EXEC_TIMEOUT_MS)
}
fn tui_permission_policy() -> a3s_code_core::permissions::PermissionPolicy {
a3s_code_core::permissions::PermissionPolicy::new().allow_all(&[
"Read(*)",
"Grep(*)",
"Glob(*)",
"LS(*)",
"read(*)",
"grep(*)",
"glob(*)",
"ls(*)",
"web_search(*)",
"web_fetch(*)",
])
}
fn instant_from_epoch_ms(epoch_ms: u64) -> Instant {
let now = Instant::now();
if epoch_ms == 0 {
return now;
}
let wall_now_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.unwrap_or(epoch_ms);
let age_ms = wall_now_ms.saturating_sub(epoch_ms);
now.checked_sub(Duration::from_millis(age_ms))
.unwrap_or(now)
}
fn touch_workspace_file_path_for_manifest(
manifest: &LocalWorkspaceManifest,
workspace: &str,
path: &Path,
) {
let root = Path::new(workspace);
if let Ok(relative) = path.strip_prefix(root) {
if let Some(path) = relative.to_str() {
manifest.touch_file(path);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RuntimeEvidenceMode {
Any,
ParallelReportView,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct RuntimeExpectation {
label: String,
policy: RuntimePolicy,
evidence_mode: RuntimeEvidenceMode,
runtime_tool: bool,
parallel_work: bool,
remote_view: bool,
warned_missing: bool,
}
impl RuntimeExpectation {
fn required(label: impl Into<String>) -> Self {
Self {
label: label.into(),
policy: RuntimePolicy::Required,
evidence_mode: RuntimeEvidenceMode::Any,
runtime_tool: false,
parallel_work: false,
remote_view: false,
warned_missing: false,
}
}
fn required_report_view(label: impl Into<String>) -> Self {
Self {
evidence_mode: RuntimeEvidenceMode::ParallelReportView,
..Self::required(label)
}
}
fn record_tool(&mut self, name: &str) {
match name {
"runtime" => self.runtime_tool = true,
"dynamic_workflow" | "parallel_task" | "task" => self.parallel_work = true,
_ => {}
}
}
fn record_parallel_work(&mut self) {
self.parallel_work = true;
}
fn record_remote_view(&mut self) {
self.remote_view = true;
}
fn has_parallel_evidence(&self) -> bool {
self.runtime_tool || self.parallel_work
}
fn is_satisfied(&self) -> bool {
match self.evidence_mode {
RuntimeEvidenceMode::Any => self.has_parallel_evidence() || self.remote_view,
RuntimeEvidenceMode::ParallelReportView => {
self.has_parallel_evidence() && self.remote_view
}
}
}
fn missing_expectation(&self) -> String {
match self.evidence_mode {
RuntimeEvidenceMode::Any => {
"expected `dynamic_workflow`, `runtime`, `parallel_task`, or an OS shaped `.view`/`viewUrl` response"
.to_string()
}
RuntimeEvidenceMode::ParallelReportView => match (self.has_parallel_evidence(), self.remote_view) {
(false, false) => {
"expected `dynamic_workflow`/OS Runtime/`parallel_task` fan-out plus an OS shaped `.view`/`viewUrl` report response".to_string()
}
(false, true) => {
"expected `dynamic_workflow`/OS Runtime/`parallel_task` fan-out before the report view".to_string()
}
(true, false) => {
"expected an OS shaped `.view`/`viewUrl` response for the report".to_string()
}
(true, true) => unreachable!("satisfied expectations are filtered before warning"),
},
}
}
fn missing_warning(&mut self) -> Option<String> {
if self.policy != RuntimePolicy::Required || self.is_satisfied() || self.warned_missing {
return None;
}
self.warned_missing = true;
Some(format!(
" Runtime evidence missing for {} - {} before the final answer",
self.label,
self.missing_expectation()
))
}
fn corrective_prompt(&self) -> Option<String> {
if self.policy != RuntimePolicy::Required || self.is_satisfied() {
return None;
}
Some(format!(
"The previous turn ended without the required OS Runtime evidence for {}: {}. \
Continue the same task, explicitly use `dynamic_workflow` first; inside it use \
the signed-in `runtime` tool or a host-side `parallel_task` step as required, \
create or surface the shaped OS `.view`/`viewUrl` report response when required, \
and only then give the final answer. If the OS capability is unavailable, explain exactly \
which OS endpoint or response field is missing and provide local report artifact paths.",
self.label,
self.missing_expectation()
))
}
}
fn is_new_remote_view(last_view: Option<&remote_ui::ViewSpec>, spec: &remote_ui::ViewSpec) -> bool {
last_view != Some(spec)
}
fn take_pending_tool_label(
pending_tool: &mut Option<(String, String)>,
tool_id: &str,
) -> Option<String> {
if pending_tool
.as_ref()
.is_some_and(|(pending_id, _)| pending_id == tool_id)
{
pending_tool.take().map(|(_, label)| label)
} else {
None
}
}
struct App {
session: Arc<AgentSession>,
agent: Arc<Agent>,
store: Arc<dyn a3s_code_core::store::SessionStore>,
confirmation: a3s_code_core::hitl::ConfirmationPolicy,
session_id: String,
models: Vec<String>,
model_ctx: std::collections::HashMap<String, u32>,
context_limit: u32,
last_prompt_tokens: usize,
ctx_warned_tier: u8,
model_menu: Option<usize>,
model_tab: usize,
llm_override: Option<Arc<dyn a3s_code_core::llm::LlmClient>>,
os_config: Option<OsConfig>,
os_session: Option<crate::a3s_os::StoredOsSession>,
os_refreshing: bool,
os_gateway_models: Option<Vec<String>>,
os_gateway_error: Option<String>,
last_view: Option<remote_ui::ViewSpec>,
runtime_expectation: Option<RuntimeExpectation>,
effort: usize,
effort_panel: Option<usize>,
theme_panel: Option<usize>,
quit_armed: Option<Instant>,
last_activity: Instant,
auto_reviewed: bool,
shell_mode: bool,
research_mode: bool,
review_pending: bool,
sleep_pending: bool,
review: Option<panels::review::ReviewState>,
flow: Option<panels::flow::FlowPanel>,
pending_flow_subcommand: Option<panels::flow::FlowSubcommand>,
agent_picker: Option<panels::agent::AgentPanel>,
pending_agent_subcommand: Option<panels::agent::AgentSubcommand>,
agent_dev: Option<panels::agent::AgentDevSession>,
mcp_picker: Option<panels::mcp::McpPanel>,
pending_mcp_subcommand: Option<panels::mcp::McpSubcommand>,
mcp_dev: Option<panels::mcp::McpDevSession>,
skill_picker: Option<panels::skill::SkillPanel>,
pending_skill_subcommand: Option<panels::skill::SkillSubcommand>,
skill_dev: Option<panels::skill::SkillDevSession>,
okf_picker: Option<panels::okf::OkfPackagePanel>,
pending_okf_subcommand: Option<panels::okf::OkfCommand>,
okf_dev: Option<panels::okf::OkfDevSession>,
review_open: bool,
ctx_ready: bool,
ctx_hits: Vec<panels::ctx::CtxHit>,
pending_ctx: Option<String>,
loop_continuation: bool,
turn_text: String,
selection: Option<Selection>,
last_workflow: Option<String>,
pending_images: Vec<a3s_code_core::llm::Attachment>,
goal: Option<String>,
goal_since: Option<Instant>,
loop_remaining: usize,
runtime: RuntimeProjection,
turn_had_agent_activity: bool,
turn_text_after_activity: bool,
ultracode_synthesis_inflight: bool,
ultracode_synthesis_used: bool,
instructions: Option<String>,
compact_summary: Option<String>,
workspace_manifest: Arc<LocalWorkspaceManifest>,
workspace_manifest_rx: SharedManifestRx,
workspace_services: Arc<WorkspaceServices>,
gradient_until: Option<Instant>,
gradient_frame: usize,
effort_anim: Option<Instant>,
btw: Option<(String, Option<String>)>,
viewport: Viewport,
textarea: Textarea,
spinner: Spinner,
streaming: StreamingMarkdown,
got_delta: bool,
compacting: Option<Instant>,
updating: Option<Instant>,
last_paint: Option<Instant>,
thinking: String,
state: State,
messages: Vec<String>,
rx: Option<SharedRx>,
stream_join: Option<StreamJoin>,
host_progress_inflight: bool,
interrupting: bool,
pending_tool: Option<(String, String)>,
approval_sel: usize,
history: Vec<String>,
history_pos: Option<usize>,
model: Option<String>,
output_tokens: usize,
stream_started: Option<Instant>,
blink_tick: u8,
anim: u8,
mode: Mode,
autonomy_restore: Option<Mode>,
queue: BinaryHeap<Queued>,
seq: u64,
running_task: Option<String>,
plan: Vec<(String, String, char, Color)>, top: Option<Vec<ProcessRow>>,
top_history: TopProcessHistory,
top_scroll: usize,
top_sel: usize,
top_focus: Option<u32>,
ide: Option<Ide>,
memory: Option<MemPanel>,
asset_list: Option<panels::asset_resources::AssetListPanel>,
runtime_activity: Option<panels::asset_resources::RuntimeActivityPanel>,
kb: Option<panels::kb::KbPanel>,
loop_panel: Option<panels::loop_engineering::LoopPanel>,
help_open: bool,
help_scroll: usize,
completed: usize,
cwd: String,
branch: Option<String>,
slash_sel: usize,
files: Vec<String>,
file_sel: usize,
at_expanded: std::collections::HashSet<String>,
skill_count: usize,
skills: Vec<(String, String)>,
disabled_skills: std::collections::HashSet<String>,
plugins_panel: Option<usize>,
update_available: Option<String>,
width: u16,
height: u16,
keymap: Keymap<Action>,
}
impl App {
pub(crate) fn touch_workspace_file(&self, path: &str) {
self.workspace_manifest.touch_file(path);
}
pub(crate) fn viewport_content_width(&self) -> usize {
viewport_content_width_for(self.width)
}
fn transcript_markdown_width(&self) -> usize {
transcript_markdown_width_for(self.width)
}
}
impl Model for App {
type Msg = Msg;
fn init(&mut self) -> Option<Cmd<Msg>> {
let mut cmds = vec![cmd::cmd(|| async {
Msg::UpdateCheck(check_latest_version().await)
})];
cmds.push(pump_manifest(self.workspace_manifest_rx.clone()));
cmds.push(banner_tick());
if self.messages.is_empty() {
self.viewport.set_content(&self.banner());
} else {
self.rebuild_viewport();
self.viewport.update(ViewportMsg::Bottom);
}
Some(cmd::batch(cmds))
}
fn update(&mut self, msg: Msg) -> Option<Cmd<Msg>> {
match msg {
Msg::Term(Event::Resize { width, height }) => {
self.selection = None; self.width = width;
self.height = height;
self.relayout();
self.textarea
.set_width(width.saturating_sub((PAD + 2) as u16));
if let Some(f) = self
.ide
.as_mut()
.and_then(|i| i.file.as_mut())
.filter(|f| f.image)
{
let inner = panels::spf::ide_split(width as usize).1.saturating_sub(2);
let body = (height as usize).saturating_sub(5);
f.lines = render_image_file(&f.path, inner, body)
.unwrap_or_else(|| vec!["<cannot decode image>".into()]);
f.scroll = 0;
}
let raw = self.streaming.raw_content().to_string();
self.streaming = StreamingMarkdown::new(self.transcript_markdown_width());
if !raw.is_empty() {
self.streaming.push(&raw);
}
self.rebuild_viewport();
}
Msg::Term(Event::Paste(text)) => {
self.last_activity = Instant::now();
if self.ide.is_some() {
self.ide_paste_text(&text);
return None;
}
self.textarea.insert_str(&text);
self.relayout();
}
Msg::Term(Event::Key(key)) => {
self.last_activity = Instant::now();
self.auto_reviewed = false;
self.selection = None;
if is_quit_key(&key) {
let now = Instant::now();
if quit_is_confirmed(self.quit_armed, now) {
return Some(cmd::quit());
}
self.quit_armed = Some(now);
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" press Ctrl+C again to exit"),
);
return None;
}
if self.btw.is_some() && key.code == KeyCode::Esc {
self.btw = None;
return None;
}
if self.help_open {
return self.handle_help_key(&key);
}
if self.memory.is_some() {
return self.memory_key(&key);
}
if self.asset_list.is_some() {
return self.handle_asset_list_key(&key);
}
if self.runtime_activity.is_some() {
return self.handle_runtime_activity_key(&key);
}
if self.kb.is_some() {
if self.state == State::Awaiting {
return self.handle_approval_key(&key);
}
return self.handle_kb_key(&key);
}
if self.ide.is_some() {
if self.state == State::Awaiting {
return self.handle_approval_key(&key);
}
self.ide_key(&key);
return None;
}
if self.top.is_some() {
let rows = self.top_rows();
let last = rows.len().saturating_sub(1);
match key.code {
KeyCode::Esc => {
if self.top_focus.is_some() {
self.top_focus = None;
self.top_sel = 0;
self.top_scroll = 0;
} else {
self.top = None;
self.top_history.clear();
}
}
KeyCode::Up | KeyCode::Char('k') => {
self.top_sel = self.top_sel.saturating_sub(1)
}
KeyCode::Down | KeyCode::Char('j') => {
self.top_sel = (self.top_sel + 1).min(last)
}
KeyCode::PageUp => self.top_sel = self.top_sel.saturating_sub(10),
KeyCode::PageDown => self.top_sel = (self.top_sel + 10).min(last),
KeyCode::Enter | KeyCode::Right if self.top_focus.is_none() => {
if let Some(row) = rows.get(self.top_sel) {
if row.agent.is_some() {
self.top_focus = Some(row.pid);
self.top_sel = 0;
self.top_scroll = 0;
}
}
}
_ => {}
}
let body = (self.height as usize).saturating_sub(3);
if self.top_sel < self.top_scroll {
self.top_scroll = self.top_sel;
} else if self.top_sel >= self.top_scroll + body {
self.top_scroll = self.top_sel + 1 - body;
}
return None;
}
if key.code == KeyCode::BackTab {
self.mode = self.mode.next();
return None;
}
if self.state == State::Awaiting {
return self.handle_approval_key(&key);
}
if self.model_menu.is_some() {
return self.handle_model_key(&key).unwrap_or(None);
}
if let Some(sel) = self.effort_panel {
match key.code {
KeyCode::Left => self.effort_panel = Some(sel.saturating_sub(1)),
KeyCode::Right => {
self.effort_panel = Some((sel + 1).min(EFFORT_LEVELS.len() - 1))
}
KeyCode::Enter => {
self.confirm_effort_selection(sel);
}
KeyCode::Esc => {
self.effort_panel = None;
self.effort_anim = None;
}
_ => {}
}
return None;
}
if let Some(sel) = self.theme_panel {
match key.code {
KeyCode::Up => self.theme_panel = Some(sel.saturating_sub(1)),
KeyCode::Down => self.theme_panel = Some((sel + 1).min(THEMES.len() - 1)),
KeyCode::Enter => self.apply_theme_selection(sel),
KeyCode::Esc => self.theme_panel = None,
_ => {}
}
return None;
}
if let Some(sel) = self.plugins_panel {
let last = self.skills.len().saturating_sub(1);
match key.code {
KeyCode::Up => self.plugins_panel = Some(sel.saturating_sub(1)),
KeyCode::Down => self.plugins_panel = Some((sel + 1).min(last)),
KeyCode::Char(' ') => {
self.toggle_plugin_skill(sel.min(last));
}
KeyCode::Esc => self.plugins_panel = None,
_ => {}
}
return None;
}
if self.review_open {
return self.handle_review_key(&key);
}
if self.flow.is_some() {
return self.handle_flow_key(&key);
}
if self.agent_picker.is_some() {
return self.handle_agent_key(&key);
}
if self.mcp_picker.is_some() {
return self.handle_mcp_key(&key);
}
if self.skill_picker.is_some() {
return self.handle_skill_key(&key);
}
if self.okf_picker.is_some() {
return self.handle_okf_package_key(&key);
}
if self.loop_panel.is_some() {
return self.handle_loop_key(&key);
}
if key.code == KeyCode::End && key.modifiers.contains(KeyModifiers::SHIFT) {
self.viewport.update(ViewportMsg::Bottom);
self.viewport.set_auto_scroll(true);
return None;
}
if let Some(action) = self.keymap.resolve(&key) {
let m = match action {
Action::ScrollUp => ViewportMsg::PageUp,
Action::ScrollDown => ViewportMsg::PageDown,
Action::ScrollTop => ViewportMsg::Top,
Action::ScrollBottom => ViewportMsg::Bottom,
};
self.viewport.update(m);
self.viewport.set_auto_scroll(self.viewport.at_bottom());
return None;
}
if (self.shell_mode || self.research_mode) && key.code == KeyCode::Esc {
self.shell_mode = false;
self.research_mode = false;
self.textarea.clear();
return None;
}
if self.state == State::Streaming && key.code == KeyCode::Esc {
if self.interrupting {
return None;
}
self.interrupting = true;
self.push_line(&Style::new().fg(TN_YELLOW).render(" ⎋ interrupting…"));
let session = self.session.clone();
let join = self.stream_join.take();
return Some(cmd::cmd(move || async move {
session.cancel().await;
if let Some(join) = join {
let _ = join.await;
}
Msg::Interrupted
}));
}
if self.state == State::Idle && self.agent_dev.is_some() && key.code == KeyCode::Esc
{
self.exit_agent_dev();
return None;
}
if self.state == State::Idle && self.mcp_dev.is_some() && key.code == KeyCode::Esc {
self.exit_mcp_dev();
return None;
}
if self.state == State::Idle && self.skill_dev.is_some() && key.code == KeyCode::Esc
{
self.exit_skill_dev();
return None;
}
if self.state == State::Idle && self.okf_dev.is_some() && key.code == KeyCode::Esc {
self.exit_okf_dev();
return None;
}
if self.slash_menu_open() {
if let Some(result) = self.handle_slash_key(&key) {
return result;
}
}
if self.file_menu_open() {
if let Some(result) = self.handle_file_key(&key) {
return result;
}
}
if matches!(key.code, KeyCode::Up | KeyCode::Down)
&& !self.textarea.value().contains('\n')
&& !self.history.is_empty()
{
self.history_recall(key.code == KeyCode::Up);
return None;
}
if key.code == KeyCode::Char('v') && key.modifiers.contains(KeyModifiers::CONTROL) {
self.paste_clipboard_image();
return None;
}
if let Some(TextareaMsg::Submit(text)) = self.textarea.handle_key(&key) {
return Some(cmd::msg(Msg::Submit(text)));
}
let val = self.textarea.value();
if !self.shell_mode && !self.research_mode {
if let Some(rest) = val.strip_prefix('!') {
self.shell_mode = true;
self.textarea.set_value(rest);
} else if let Some(rest) = val.strip_prefix('?') {
self.research_mode = true;
self.textarea.set_value(rest);
}
}
}
Msg::Term(Event::Mouse(m)) => {
use a3s_tui::event::{MouseButton, MouseEventKind};
if self.state == State::Awaiting {
return self.handle_approval_mouse(&m);
}
if self.model_menu.is_some() {
self.handle_model_mouse(&m);
return None;
}
if self.effort_panel.is_some() {
self.handle_effort_mouse(&m);
return None;
}
if self.theme_panel.is_some() {
self.handle_theme_mouse(&m);
return None;
}
if self.file_menu_open() {
self.handle_file_mouse(&m);
return None;
}
if self.plugins_panel.is_some() {
self.handle_plugins_mouse(&m);
return None;
}
if self.slash_menu_open() {
return self.handle_slash_mouse(&m);
}
if self.flow.is_some() {
return self.handle_flow_mouse(&m);
}
if self.agent_picker.is_some() {
return self.handle_agent_mouse(&m);
}
if self.mcp_picker.is_some() {
return self.handle_mcp_mouse(&m);
}
if self.skill_picker.is_some() {
return self.handle_skill_mouse(&m);
}
if self.okf_picker.is_some() {
return self.handle_okf_package_mouse(&m);
}
if self.top.is_some() {
return self.handle_top_mouse(&m);
}
if self.help_open {
match m.kind {
MouseEventKind::ScrollUp => self.scroll_help_by(-3),
MouseEventKind::ScrollDown => self.scroll_help_by(3),
_ => {}
}
return None;
}
if self.ide.is_some()
|| self.kb.is_some()
|| self.asset_list.is_some()
|| self.runtime_activity.is_some()
{
return None;
}
let vp_rows = self.viewport_rows();
let max_col = (self.width as usize).saturating_sub(2) as u16;
match m.kind {
MouseEventKind::ScrollUp => {
self.selection = None;
self.viewport.update(ViewportMsg::ScrollUp(3));
}
MouseEventKind::ScrollDown => {
self.selection = None;
self.viewport.update(ViewportMsg::ScrollDown(3));
}
MouseEventKind::Down(MouseButton::Left) => {
self.selection = viewport_mouse_cell(m.row, m.column, vp_rows, max_col)
.map(|p| Selection { anchor: p, head: p });
}
MouseEventKind::Drag(MouseButton::Left) => {
if let Some(s) = self.selection.as_mut() {
if let Some(p) =
viewport_mouse_cell_clamped(m.row, m.column, vp_rows, max_col)
{
s.head = p;
}
}
}
MouseEventKind::Up(MouseButton::Left) => {
if let Some(mut s) = self.selection {
if let Some(p) =
viewport_mouse_cell_clamped(m.row, m.column, vp_rows, max_col)
{
s.head = p;
}
let view = self.viewport.view();
if is_remote_view_click(&view, s) {
self.selection = None;
if let Some(spec) = self.last_view.clone() {
self.open_remote_view(&spec);
} else {
self.push_line(&Style::new().fg(TN_GRAY).render(
" no OS view is available for this Open view marker yet",
));
}
} else if s.is_empty() {
self.selection = None;
} else {
let (r1, c1, r2, c2) = s.ordered();
let text = selection_to_text(&view, r1, c1, r2, c2);
if text.trim().is_empty() {
self.selection = None;
} else {
copy_to_clipboard(&text);
}
}
}
}
_ => {}
}
self.viewport.set_auto_scroll(self.viewport.at_bottom());
}
Msg::Submit(text) => return self.on_submit(text),
Msg::StreamStarted(rx, join) => {
self.rx = Some(rx.clone());
self.stream_join = Some(join);
self.host_progress_inflight = false;
self.interrupting = false;
return Some(pump(rx));
}
Msg::StreamError(e) => {
self.push_line(&Style::new().fg(TN_RED).render(&format!(" error: {e}")));
self.loop_remaining = 0; self.review_pending = false; self.sleep_pending = false; self.restore_autonomy();
self.finish();
return self.drain_queue();
}
Msg::WorkspaceManifest(snapshot) => {
self.files = snapshot.file_paths();
self.file_sel = self.file_sel.min(self.files.len().saturating_sub(1));
return Some(pump_manifest(self.workspace_manifest_rx.clone()));
}
Msg::WorkspaceManifestStopped => {
let snapshot = self.workspace_manifest.snapshot();
self.files = snapshot.file_paths();
self.file_sel = self.file_sel.min(self.files.len().saturating_sub(1));
}
Msg::Interrupted => {
self.finalize_streaming();
self.push_line(&Style::new().fg(TN_YELLOW).render(" ⎋ interrupted"));
self.loop_remaining = 0; self.review_pending = false; self.sleep_pending = false; self.restore_autonomy();
self.finish();
return self.drain_queue();
}
Msg::Agent(event) => return self.on_agent_event(*event),
Msg::StreamEnded => {
if self.host_progress_inflight {
self.rx = None;
return None;
}
if self.interrupting || self.state != State::Streaming {
return None;
}
self.finalize_streaming();
let turn_text = self.turn_text.clone();
self.capture_review(&turn_text);
let sleep_save = self.capture_sleep(&turn_text);
self.disarm_sleep_if_over(sleep_save.is_some());
return match (sleep_save, self.complete_turn()) {
(Some(save), Some(next)) => Some(cmd::batch(vec![save, next])),
(save, next) => save.or(next),
};
}
Msg::SpinnerTick => {
self.spinner.tick();
self.blink_tick = self.blink_tick.wrapping_add(1);
if self.state == State::Streaming {
self.update_viewport_with_stream();
return Some(spinner_tick());
}
}
Msg::BannerTick => {
if self.messages.is_empty()
&& self.state == State::Idle
&& self.top.is_none()
&& self.ide.is_none()
&& self.memory.is_none()
&& !self.help_open
{
self.anim = self.anim.wrapping_add(1);
self.viewport.set_content(&self.banner());
}
if self.gradient_until.is_some() || self.effort_anim.is_some() {
self.gradient_frame = self.gradient_frame.wrapping_add(1);
}
if let Some(t) = self.effort_anim {
if t.elapsed() > Duration::from_millis(1100) {
self.effort_anim = None;
self.effort_panel = None;
self.apply_effort();
}
}
if !self.auto_reviewed
&& self.state == State::Idle
&& !self.messages.is_empty()
&& self.last_activity.elapsed() > Duration::from_secs(300)
{
self.auto_reviewed = true;
let agent = self.agent.clone();
let workspace = self.cwd.clone();
let history = self.session.history();
let review = cmd::cmd(move || async move {
let conf = a3s_code_core::hitl::ConfirmationPolicy::enabled()
.with_timeout(BACKGROUND_CONFIRM_TIMEOUT_MS, TimeoutAction::Reject);
let prompt = "Briefly review this conversation so far: summarise the \
key decisions and what's done, then list any open threads or next \
steps. Keep it to a few lines.";
let mut answer = String::new();
if let Ok(sess) = agent.session(workspace, Some(tui_session_options(conf)))
{
if let Ok((mut rx, _j)) = sess.stream(prompt, Some(&history)).await {
while let Some(ev) = rx.recv().await {
match ev {
AgentEvent::TextDelta { text } => answer.push_str(&text),
AgentEvent::End { text, .. } => {
if answer.trim().is_empty() {
answer = text;
}
break;
}
_ => {}
}
}
}
}
Msg::AutoReview(answer)
});
return Some(cmd::batch(vec![banner_tick(), review]));
}
if !self.os_refreshing {
if let Some(s) = &self.os_session {
if crate::a3s_os::needs_refresh(s) {
self.os_refreshing = true;
let session = s.clone();
let refresh = cmd::cmd(move || async move {
Msg::OsRefreshed(
crate::a3s_os::refresh_session(&session)
.await
.map_err(|e| e.to_string()),
)
});
return Some(cmd::batch(vec![banner_tick(), refresh]));
}
}
}
return Some(banner_tick());
}
Msg::AutoReview(text) => {
if !text.trim().is_empty() {
let dim =
|s: &str| format!(" {}", Style::new().fg(TN_GRAY).italic().render(s));
let mut lines = vec![dim("⟳ inactivity review")];
lines.extend(text.trim().lines().map(dim));
self.push_line(&lines.join("\n"));
}
}
Msg::Compacted(summary) => {
self.compacting = None;
if summary.trim().is_empty() {
self.push_line(
&Style::new()
.fg(TN_RED)
.render(" compaction failed (empty summary)"),
);
return None;
}
self.compact_summary = Some(summary.trim().to_string());
self.session_id = new_session_id();
let model = self.model.clone();
match self.rebuild_session(model.as_deref()) {
Ok((s, _)) => {
self.replace_session(s);
self.messages.clear();
self.output_tokens = 0;
self.last_prompt_tokens = 0;
self.ctx_warned_tier = 0; self.push_line(
&Style::new()
.fg(TN_GREEN)
.bold()
.render(" ✦ context compacted — continuing from this summary:"),
);
self.push_line(&gutter(
TN_CYAN,
self.compact_summary.as_deref().unwrap_or(""),
));
self.rebuild_viewport();
}
Err(e) => self.push_line(
&Style::new()
.fg(TN_RED)
.render(&format!(" compaction failed: {e}")),
),
}
}
Msg::UpdateCheck(latest) => {
let current = crate::update::current_version();
let newer = latest
.as_deref()
.is_some_and(|l| !crate::update::version_ge(¤t, l));
if newer {
self.update_available = latest;
if self.messages.is_empty() {
self.viewport.set_content(&self.banner());
}
}
}
Msg::ModalConfirm(idx) => {
let approved = idx == 0;
self.state = State::Streaming;
if let Some((tool_id, label)) = self.pending_tool.take() {
if !approved {
self.push_line(
&Style::new()
.fg(TN_RED)
.render(&format!(" ⎿ denied {label}")),
);
}
let session = self.session.clone();
return Some(cmd::batch(vec![
cmd::cmd(move || async move {
let _ = session.confirm_tool_use(&tool_id, approved, None).await;
Msg::Resume
}),
spinner_tick(),
]));
}
}
Msg::Resume => {
if let Some(rx) = self.rx.clone() {
return Some(pump(rx));
}
}
Msg::ShellOutput(text) => {
let body = text.lines().take(40).collect::<Vec<_>>().join("\n");
self.push_line(&gutter(TN_GRAY, body.trim_end()));
}
Msg::DeepResearchWorkflowCompleted {
query,
os_runtime,
args,
result,
} => return self.on_deep_research_workflow_completed(query, os_runtime, args, result),
Msg::UpdatePlan(latest) => {
self.updating = None;
self.relayout();
let current = crate::update::current_version();
match latest {
None => self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" couldn't reach the release server — try again later"),
),
Some(l) if crate::update::version_ge(¤t, &l) => {
self.push_line(
&Style::new()
.fg(TN_GREEN)
.render(&format!(" ✓ already up to date (a3s {current})")),
);
self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" checking companion tools…"),
);
self.updating = Some(Instant::now());
self.relayout();
return Some(cmd::cmd(|| async {
let result =
tokio::task::spawn_blocking(crate::update::repair_installation)
.await
.map_err(|e| format!("repair task failed: {e}"))
.and_then(|r| r);
Msg::UpdateRepair(result)
}));
}
Some(l) => {
if crate::update::can_self_update() {
if let Ok(mut g) = LATEST.lock() {
*g = Some(l.clone());
}
UPGRADE_ON_EXIT.store(true, std::sync::atomic::Ordering::Relaxed);
self.push_line(&Style::new().fg(TN_GREEN).render(&format!(
" → a3s {l} available — closing to upgrade, then restarting…"
)));
return Some(cmd::quit());
}
self.push_line(&Style::new().fg(TN_GRAY).render(&format!(
" → a3s {l} available — download: https://github.com/A3S-Lab/Cli/releases/latest"
)));
}
}
}
Msg::UpdateRepair(result) => {
self.updating = None;
self.relayout();
match result {
Ok(items) if items.is_empty() => self.push_line(
&Style::new()
.fg(TN_GREEN)
.render(" ✓ installation looks healthy"),
),
Ok(items) => {
for item in items {
self.push_line(
&Style::new().fg(TN_GREEN).render(&format!(" ✓ {item}")),
);
}
}
Err(error) => self.push_line(
&Style::new()
.fg(TN_RED)
.render(&format!(" install repair failed: {error}")),
),
}
}
Msg::OsLogin(result) => match result {
Ok(label) => {
self.os_session = self
.os_config
.as_ref()
.and_then(crate::a3s_os::current_session);
if let Some(s) = &self.os_session {
crate::a3s_os::export_os_env(s);
}
self.refresh_after_auth();
self.push_line(&Style::new().fg(TN_GREEN).render(&format!(
" ✓ signed in to OS as {label} · capabilities skill active"
)));
if let Some(s) = self.os_session.clone() {
return Some(cmd::cmd(move || async move {
Msg::SshKeySynced(crate::a3s_os::sync_ssh_key(s).await)
}));
}
}
Err(error) => self.push_line(
&Style::new()
.fg(TN_RED)
.render(&format!(" login failed: {error}")),
),
},
Msg::SshKeySynced(outcome) => {
use crate::a3s_os::SshKeyOutcome;
match outcome {
SshKeyOutcome::Registered(fp) => self.push_line(&Style::new().fg(TN_GREEN).render(
&format!(" ✓ local SSH public key registered with OS ({fp}) · git clone(ssh) ready"),
)),
SshKeyOutcome::AlreadyRegistered => self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" · SSH public key already registered with OS; skipping"),
),
SshKeyOutcome::NoLocalKey => self.push_line(&Style::new().fg(TN_YELLOW).render(
" · no local SSH public key found; create one and run /login again to register it automatically: ssh-keygen -t ed25519",
)),
SshKeyOutcome::Failed(e) => self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(&format!(" · SSH key sync skipped: {e}")),
),
}
}
Msg::OsRefreshed(result) => {
self.os_refreshing = false;
match result {
Ok(session) => {
crate::a3s_os::export_os_env(&session);
self.os_session = Some(session);
self.sync_runtime_tool();
}
Err(_) => {
}
}
}
Msg::OsGatewayModels(result) => {
match result {
Ok(models) => {
for m in &models {
if let Some(ctx) = m.context {
self.model_ctx.insert(m.id.clone(), ctx);
}
}
self.os_gateway_models = Some(models.into_iter().map(|m| m.id).collect());
self.os_gateway_error = None;
}
Err(e) => {
self.os_gateway_models = Some(Vec::new());
self.os_gateway_error = Some(e);
}
}
self.open_model_menu();
}
Msg::SideNote(text) => {
if let Some((q, _)) = self.btw.take() {
self.btw = Some((q, Some(text.trim().to_string())));
}
}
Msg::TopData(rows) => {
if self.top.is_some() {
self.top_history.observe(&rows);
self.top = Some(rows);
return Some(cmd::tick(Duration::from_millis(1500), Msg::TopRefresh));
}
}
Msg::TopRefresh => {
if self.top.is_some() {
return Some(cmd::cmd(|| async { Msg::TopData(fetch_top().await) }));
}
}
Msg::Forked(result) => {
match result {
Ok(new_id) => {
let prev = std::mem::replace(&mut self.session_id, new_id);
let model = self.model.clone();
match self.rebuild_session(model.as_deref()) {
Ok((s, _)) => {
self.replace_session(s);
let short: String = self.session_id.chars().take(8).collect();
self.push_line(&gutter(
TN_CYAN,
&format!("⑂ forked into a new session ({short}) — the original is kept"),
));
}
Err(e) => {
self.session_id = prev;
self.push_line(
&Style::new()
.fg(TN_RED)
.render(&format!(" fork failed: {e}")),
);
}
}
}
Err(e) => {
self.push_line(&Style::new().fg(TN_YELLOW).render(&format!(" /fork: {e}")))
}
}
}
Msg::MemoryLoaded(data) => {
if let Some(m) = &mut self.memory {
let source = if data.loaded_from_session {
"session fallback · "
} else {
""
};
m.note = format!(
"{source}{} memories · {} entities · {} relations",
data.entries.len(),
data.graph.stats.entities,
data.graph.stats.relations
);
m.sel = 0;
m.apply_data(data);
}
}
Msg::MemoryForgotten(result) => {
if let Some(m) = &mut self.memory {
match result {
Ok((id, data)) => {
m.note = format!(
"forgot {id} · {} memories · {} candidates",
data.entries.len(),
data.graph.stats.forget_candidates
);
m.apply_data(data);
}
Err(error) => {
m.note = format!("forget failed: {error}");
}
}
}
}
Msg::AssetListLoaded(result) => self.on_asset_list(result),
Msg::RuntimeActivityLoaded(result) => self.on_runtime_activity(result),
Msg::KbAdded(summary) => {
let color = if summary.starts_with('✗') {
TN_RED
} else {
TN_GRAY
};
self.push_line(&Style::new().fg(color).render(&format!(" {summary}")));
if self.kb.is_some() {
self.open_kb_home(Some(summary));
}
}
Msg::CtxResults(res) => self.on_ctx_results(res),
Msg::CtxWindow(res) => self.on_ctx_window(res),
Msg::CtxSaved(res) => self.on_ctx_saved(res),
Msg::SleepSaved(res) => self.on_sleep_saved(res),
Msg::FlowOsCompleted(res) => self.on_flow_os_completed(res),
Msg::AgentOsCompleted(res) => self.on_agent_os_completed(res),
Msg::McpOsCompleted(res) => self.on_mcp_os_completed(res),
Msg::SkillOsCompleted(res) => self.on_skill_os_completed(res),
Msg::OkfOsCompleted(res) => self.on_okf_os_completed(res),
Msg::AssetCloned(res) => match res {
Ok(result) => self.on_asset_cloned(result),
Err(error) => self.push_line(
&Style::new()
.fg(TN_RED)
.render(&format!(" clone failed: {error}")),
),
},
Msg::CtxMemorySource(res) => match res {
Ok((event_id, window)) => {
self.memory = None; self.open_readonly_in_ide(&format!("ctx-source-{event_id}.txt"), &window);
}
Err(e) => {
if let Some(m) = self.memory.as_mut() {
m.note = format!("ctx source unavailable: {e}");
}
}
},
_ => {}
}
None
}
fn view(&self) -> String {
if self.help_open {
return self.render_help();
}
if let Some(m) = &self.memory {
return self.render_memory(m);
}
if let Some(panel) = &self.asset_list {
return self.render_asset_list(panel);
}
if let Some(panel) = &self.runtime_activity {
return self.render_runtime_activity(panel);
}
if let Some(kb) = &self.kb {
let page = self.render_kb(kb);
return self.overlay_approval(page);
}
if let Some(panel) = &self.loop_panel {
return self.render_loop_panel(panel);
}
if let Some(ide) = &self.ide {
let page = self.render_ide(ide);
return self.overlay_approval(page);
}
if self.top.is_some() {
return self.render_top_panel();
}
let width = self.width as usize;
let raw_view = self.viewport.view();
let shown = match &self.selection {
Some(s) if !s.is_empty() => {
let (r1, c1, r2, c2) = s.ordered();
highlight_selection(&raw_view, r1, c1, r2, c2)
}
_ => raw_view,
};
let viewport_view = append_scrollbar(
&shown,
width.saturating_sub(1),
self.viewport.total_lines(),
self.viewport.scroll_percent(),
);
let inp = self.textarea.value();
let (sym, icolor, border): (&str, Color, Color) = if self.shell_mode {
("!", GRADIENT_PREVIEW_END, GRADIENT_PREVIEW_END)
} else if self.research_mode {
("?", TN_CYAN, TN_CYAN)
} else if self.agent_dev.is_some() {
("◇", TN_GREEN, TN_GREEN)
} else if self.mcp_dev.is_some() {
("◆", TN_CYAN, TN_CYAN)
} else if self.skill_dev.is_some() {
("✦", TN_CYAN, TN_CYAN)
} else if self.okf_dev.is_some() {
("⌁", TN_CYAN, TN_CYAN)
} else if inp.starts_with("/btw") {
("❯", TN_YELLOW, TN_YELLOW)
} else {
("❯", ACCENT, TN_GRAY)
};
let gradient = self
.gradient_until
.is_some_and(|t| t.elapsed() < Duration::from_millis(1600));
let separator = if gradient {
input_gradient_rule(width, &BRAND_GRADIENT, self.gradient_frame + 3)
} else {
input_rule(width, border)
};
let top_separator = if gradient {
input_gradient_rule(width, &BRAND_GRADIENT, self.gradient_frame)
} else {
let elabel = format!("◇ {}", EFFORT_LEVELS[self.effort].label);
let ctxlabel = if self.context_limit > 0 {
let pct = (self.last_prompt_tokens * 100 / self.context_limit as usize).min(100);
format!("{pct}% context used ")
} else {
String::new()
};
input_status_rule(width, border, &ctxlabel, &elabel)
};
let activity = if self.updating.is_some() {
Style::new()
.fg(TN_GREEN)
.render(" ⬇ checking for updates…")
} else if let Some(t0) = self.compacting {
compact_progress_line(t0.elapsed(), width)
} else {
match self.state {
State::Streaming => {
let g = ['✶', '✸', '✹', '✺', '✹', '✷'][(self.blink_tick as usize / 2) % 6];
let spark = Style::new().fg(ACCENT).render(&g.to_string());
let working = shimmer("Working…", self.blink_tick as usize);
let mut tail = String::new();
if let Some(t0) = self.stream_started {
let est = self.output_tokens
+ estimate_tokens(self.streaming.raw_content())
+ estimate_tokens(&self.thinking);
tail.push_str(&format!(" ({}", fmt_elapsed(t0.elapsed())));
if est > 0 {
tail.push_str(&format!(" · ↓ {} tokens", humanize(est)));
}
tail.push(')');
}
let tail = Style::new().fg(ACCENT).render(&tail);
format!(" {spark} {working}{tail}")
}
State::Awaiting => String::new(),
State::Idle => String::new(),
}
};
let typed = self.textarea.view();
let tint_input = sym == "!"
|| sym == "?"
|| sym == "◇"
|| sym == "◆"
|| sym == "⌁"
|| inp.starts_with("/btw");
let input_view = input_prompt_line(sym, icolor, &typed, tint_input, width);
let status1 = self.session_status_line(width);
let status2 = render_mode_status_line(self.mode, width);
let spacer = if self.viewport.at_bottom() {
String::new()
} else {
jump_to_latest_hint(width)
};
let tasks = self.task_lines();
let task_block = tasks.join("\n");
let plan = self.plan_lines();
let plan_block = plan.join("\n");
let subs = self.subagent_lines();
let sub_block = subs.join("\n");
let composed = Layout::vertical()
.item(&viewport_view, Constraint::Fill)
.item(&spacer, Constraint::Fixed(1))
.item(&activity, Constraint::Fixed(1))
.item(&plan_block, Constraint::Fixed(plan.len() as u16))
.item(&top_separator, Constraint::Fixed(1))
.item(&input_view, Constraint::Fixed(self.input_height()))
.item(&separator, Constraint::Fixed(1))
.item(&status1, Constraint::Fixed(1))
.item(&status2, Constraint::Fixed(1))
.item(&sub_block, Constraint::Fixed(subs.len() as u16))
.item(&task_block, Constraint::Fixed(tasks.len() as u16))
.render(self.height);
let composed = self.overlay_slash_menu(composed);
let composed = self.overlay_file_menu(composed);
let composed = self.overlay_model_menu(composed);
let composed = self.overlay_review_menu(composed);
let composed = self.overlay_flow_menu(composed);
let composed = self.overlay_agent_menu(composed);
let composed = self.overlay_mcp_menu(composed);
let composed = self.overlay_skill_menu(composed);
let composed = self.overlay_okf_package_menu(composed);
let composed = self.overlay_effort(composed);
let composed = self.overlay_theme(composed);
let composed = self.overlay_plugins(composed);
let composed = self.overlay_approval(composed);
self.overlay_btw(composed)
}
fn cursor(&self) -> Option<(u16, u16)> {
if let Some(ide) = &self.ide {
if ide.focus_editor {
if let Some(f) = &ide.file {
let width = self.width as usize;
let (tw, _) = panels::spf::ide_split(width);
let gutter = if panels::spf::ide_gutter_on(width) {
5
} else {
0
};
let x = tw + 1 + gutter + f.display_col().saturating_sub(f.hscroll);
let col = x.min(width.saturating_sub(2)) as u16;
let row = (1 + f.row.saturating_sub(f.scroll)) as u16;
return Some((col, row));
}
}
return None;
}
if self.state == State::Awaiting
|| self.top.is_some()
|| self.memory.is_some()
|| self.asset_list.is_some()
|| self.runtime_activity.is_some()
|| self.kb.is_some()
|| self.loop_panel.is_some()
|| self.flow.is_some()
|| self.agent_picker.is_some()
|| self.mcp_picker.is_some()
|| self.skill_picker.is_some()
|| self.help_open
{
return None;
}
let below = 3 + self.subagent_lines().len() as u16 + self.task_lines().len() as u16;
let row = self.height.saturating_sub(below + self.input_height())
+ self.textarea.cursor_row() as u16;
let col = (PAD + 2) as u16 + self.textarea.cursor_display_col() as u16; Some((col, row))
}
}
#[allow(clippy::too_many_arguments)]
fn render_session_status_line(
cwd: &str,
branch: Option<&str>,
model: Option<&str>,
context_limit: u32,
last_prompt_tokens: usize,
output_tokens: usize,
chips: impl IntoIterator<Item = SessionStatusChip>,
width: usize,
) -> String {
if width == 0 {
return String::new();
}
let theme = agent_chrome_theme();
let chrome = agent_chrome(&theme);
let mut status = chrome
.session_status(cwd)
.branch_color(TN_YELLOW)
.threshold_colors(TN_YELLOW, TN_RED)
.margin(2);
if let Some(branch) = branch {
status = status.branch(branch);
}
if let Some(model) = model {
status = status.model(model);
}
if context_limit > 0 {
status = status.context(last_prompt_tokens, context_limit as usize);
} else if output_tokens > 0 {
status = status.output_tokens(output_tokens);
}
for chip in chips {
status = status.status_chip(chip);
}
status.view(width.min(u16::MAX as usize) as u16)
}
fn render_mode_status_line(mode: Mode, width: usize) -> String {
if width == 0 {
return String::new();
}
let theme = agent_chrome_theme();
let chrome = agent_chrome(&theme);
chrome
.mode_line(mode.name())
.glyph(mode.glyph())
.hints("(shift+tab to cycle) · /help · ↑↓ history · esc")
.mode_color(mode.color())
.view(width.min(u16::MAX as usize) as u16)
}
fn jump_to_latest_hint(width: usize) -> String {
if width == 0 {
return String::new();
}
let label = InlineAction::new("more below · Shift+End to jump to latest")
.icon("↓")
.colors(Color::BrightWhite, ACCENT)
.view_with_width(width.min(u16::MAX as usize) as u16);
let pad = width.saturating_sub(a3s_tui::style::visible_len(&label)) / 2;
format!("{}{}", " ".repeat(pad), label)
}
impl App {
fn session_status_line(&self, width: usize) -> String {
render_session_status_line(
&self.cwd,
self.branch.as_deref(),
self.model.as_deref(),
self.context_limit,
self.last_prompt_tokens,
self.output_tokens,
self.session_status_chips(),
width,
)
}
fn session_status_chips(&self) -> Vec<SessionStatusChip> {
let mut chips = Vec::new();
if self.goal.is_some() {
let elapsed = self
.goal_since
.map(|t| format!(" ({})", fmt_elapsed(t.elapsed())))
.unwrap_or_default();
chips.push(
SessionStatusChip::new("🎯", format!("Pursuing goal{elapsed}")).color(TN_CYAN),
);
}
if let Some(dev) = &self.agent_dev {
chips.push(
SessionStatusChip::new(
"◇",
format!("agent:{} · Esc /agent off", truncate(&dev.name, 24)),
)
.color(TN_GREEN),
);
}
if let Some(dev) = &self.mcp_dev {
chips.push(
SessionStatusChip::new(
"◆",
format!("mcp:{} · Esc /mcp off", truncate(&dev.name, 24)),
)
.color(TN_CYAN),
);
}
if let Some(dev) = &self.skill_dev {
chips.push(
SessionStatusChip::new(
"✦",
format!("skill:{} · Esc /skill off", truncate(&dev.name, 24)),
)
.color(TN_CYAN),
);
}
if let Some(dev) = &self.okf_dev {
chips.push(
SessionStatusChip::new(
"⌁",
format!("okf:{} · Esc /okf off", truncate(&dev.name, 24)),
)
.color(TN_CYAN),
);
}
if self.loop_remaining > 0 {
chips.push(SessionStatusChip::new("↻", self.loop_remaining.to_string()).color(TN_GRAY));
}
let active_subagents = self.runtime.active_subagent_count();
if active_subagents > 0 {
chips.push(
SessionStatusChip::new("⇉", format!("{active_subagents} agents")).color(TN_GRAY),
);
}
let active_tools = self.runtime.active_tool_count();
if active_tools > 0 {
chips.push(
SessionStatusChip::new("⚙", format!("{active_tools} running")).color(TN_GRAY),
);
}
if let Some(version) = self.update_available.as_deref() {
chips.push(SessionStatusChip::new("⬆", version).color(TN_YELLOW));
}
chips
}
fn clone_asset_command(
&mut self,
family: &'static str,
url: String,
root: std::path::PathBuf,
) -> Option<Cmd<Msg>> {
self.push_line(&Style::new().fg(TN_GRAY).render(&format!(
" cloning {family} asset from {url} → {}",
root.display()
)));
Some(cmd::cmd(move || async move {
Msg::AssetCloned(asset_clone::clone_asset_source(family, url, root).await)
}))
}
fn on_asset_cloned(&mut self, result: asset_clone::AssetCloneResult) {
self.push_line(&Style::new().fg(TN_GREEN).render(&format!(
" cloned {} asset → {}",
result.family,
result.path.display()
)));
match result.family {
"agent" => self.open_agent_panel_focused(&result.path),
"mcp" => self.open_mcp_panel_focused(&result.path),
"skill" => self.open_skill_panel_focused(&result.path),
"okf" | "knowledge" => self.open_okf_package_panel_focused(&result.path),
"workflow" => self.open_flow_panel_focused(&result.path),
_ => self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" run the asset command again to select or operate on it"),
),
}
}
fn path_is_within(path: &std::path::Path, root: &std::path::Path) -> bool {
path == root || path.starts_with(root)
}
fn open_agent_panel_focused(&mut self, cloned_path: &std::path::Path) {
let root = agent_dir();
let agents = panels::agent::list_agents(&root);
let Some(sel) = agents
.iter()
.position(|agent| Self::path_is_within(&agent.path, cloned_path))
else {
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" cloned source does not contain a recognized agent definition yet"),
);
return;
};
self.agent_picker = Some(panels::agent::AgentPanel { root, agents, sel });
self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" selected cloned agent asset · Enter develops locally"),
);
}
fn open_mcp_panel_focused(&mut self, cloned_path: &std::path::Path) {
let root = mcp_dir();
let projects = panels::mcp::list_mcp_projects(&root);
let Some(sel) = projects
.iter()
.position(|project| Self::path_is_within(&project.path, cloned_path))
else {
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" cloned source does not contain a recognized MCP asset yet"),
);
return;
};
self.mcp_picker = Some(panels::mcp::McpPanel {
root,
projects,
sel,
});
self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" selected cloned MCP asset · Enter develops locally"),
);
}
fn open_skill_panel_focused(&mut self, cloned_path: &std::path::Path) {
let root = skill_dir();
let skills = panels::skill::list_skill_assets(&root);
let Some(sel) = skills
.iter()
.position(|skill| Self::path_is_within(&skill.path, cloned_path))
else {
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" cloned source does not contain a recognized skill asset yet"),
);
return;
};
self.skill_picker = Some(panels::skill::SkillPanel { root, skills, sel });
self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" selected cloned skill asset · Enter develops locally"),
);
}
fn open_okf_package_panel_focused(&mut self, cloned_path: &std::path::Path) {
let root = panels::okf::okf_package_dir(&self.cwd);
let packages = panels::okf::list_okf_packages(&root);
let Some(sel) = packages
.iter()
.position(|package| Self::path_is_within(&package.path, cloned_path))
else {
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" cloned source does not contain a recognized OKF package yet"),
);
return;
};
self.okf_picker = Some(panels::okf::OkfPackagePanel {
root,
packages,
sel,
});
self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" selected cloned OKF package · Enter develops locally"),
);
}
fn open_flow_panel_focused(&mut self, cloned_path: &std::path::Path) {
let root = flow_dir();
let flows = panels::flow::list_flows(&root);
let Some(sel) = flows
.iter()
.position(|flow| Self::path_is_within(&root.join(flow), cloned_path))
else {
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" cloned source does not contain a recognized workflow design yet"),
);
return;
};
self.flow = Some(panels::flow::FlowPanel { root, flows, sel });
self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" selected cloned workflow asset · Enter opens the OS designer"),
);
}
fn execute_agent_subcommand(
&mut self,
subcommand: panels::agent::AgentSubcommand,
) -> Option<Cmd<Msg>> {
match subcommand {
panels::agent::AgentSubcommand::Exit => {
self.exit_agent_dev();
None
}
panels::agent::AgentSubcommand::Clone(url) => {
self.clone_asset_command("agent", url, agent_dir())
}
panels::agent::AgentSubcommand::List(query) => {
self.open_asset_list_panel(os_asset_category_query("agent", &query))
}
panels::agent::AgentSubcommand::Activity(query) => {
let Some(agent_dev) = self.agent_dev.clone() else {
self.pending_agent_subcommand =
Some(panels::agent::AgentSubcommand::Activity(query));
self.open_agent_panel();
return None;
};
self.open_runtime_activity_panel(runtime_asset_query(
"agent",
&agent_dev.name,
&query,
))
}
panels::agent::AgentSubcommand::Review => {
let Some(agent_dev) = self.agent_dev.clone() else {
self.pending_agent_subcommand = Some(panels::agent::AgentSubcommand::Review);
self.open_agent_panel();
return None;
};
self.messages
.push(user_bubble("/agent review", self.viewport_content_width()));
self.engage_autonomy(4);
self.review_pending = true;
let prompt = panels::agent::agent_review_prompt(&agent_dev);
let display = format!("◇ {} review", agent_dev.name);
self.start_stream_inner(prompt, display, true, true, false)
}
other => {
let Some(agent_dev) = self.agent_dev.clone() else {
self.pending_agent_subcommand = Some(other);
self.open_agent_panel();
return None;
};
let Some(session) = self.os_session.clone() else {
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" /agent OS actions need /login first"),
);
return None;
};
let action = match other {
panels::agent::AgentSubcommand::Publish(kind) => {
panels::agent::AgentOsAction::Publish(kind)
}
panels::agent::AgentSubcommand::Run => panels::agent::AgentOsAction::Run,
panels::agent::AgentSubcommand::Deploy => panels::agent::AgentOsAction::Deploy,
panels::agent::AgentSubcommand::Open(kind) => {
panels::agent::AgentOsAction::Open(kind)
}
panels::agent::AgentSubcommand::Logs(kind) => {
panels::agent::AgentOsAction::Logs(kind)
}
panels::agent::AgentSubcommand::Status(kind) => {
panels::agent::AgentOsAction::Status(kind)
}
panels::agent::AgentSubcommand::Exit
| panels::agent::AgentSubcommand::Clone(_)
| panels::agent::AgentSubcommand::List(_)
| panels::agent::AgentSubcommand::Activity(_)
| panels::agent::AgentSubcommand::Review => unreachable!(),
};
let kind = action.target_kind();
self.push_line(&Style::new().fg(TN_GRAY).render(&format!(
" ◇ {} → OS {} {}…",
agent_dev.name,
kind.label(),
kind.service_label()
)));
Some(cmd::cmd(move || async move {
let result =
panels::agent::publish_agent_to_os(session, agent_dev, action).await;
Msg::AgentOsCompleted(result)
}))
}
}
}
fn execute_mcp_subcommand(
&mut self,
subcommand: panels::mcp::McpSubcommand,
) -> Option<Cmd<Msg>> {
match subcommand {
panels::mcp::McpSubcommand::Exit => {
self.exit_mcp_dev();
None
}
panels::mcp::McpSubcommand::Clone(url) => {
self.clone_asset_command("mcp", url, mcp_dir())
}
panels::mcp::McpSubcommand::List(query) => {
self.open_asset_list_panel(os_asset_category_query("mcp", &query))
}
panels::mcp::McpSubcommand::Activity(query) => {
let Some(mcp_dev) = self.mcp_dev.clone() else {
self.pending_mcp_subcommand = Some(panels::mcp::McpSubcommand::Activity(query));
self.open_mcp_panel();
return None;
};
self.open_runtime_activity_panel(runtime_asset_query("mcp", &mcp_dev.name, &query))
}
panels::mcp::McpSubcommand::Review => {
let Some(mcp_dev) = self.mcp_dev.clone() else {
self.pending_mcp_subcommand = Some(panels::mcp::McpSubcommand::Review);
self.open_mcp_panel();
return None;
};
self.messages
.push(user_bubble("/mcp review", self.viewport_content_width()));
self.engage_autonomy(4);
self.review_pending = true;
let prompt = panels::mcp::mcp_review_prompt(&mcp_dev);
let display = format!("◆ {} review", mcp_dev.name);
self.start_stream_inner(prompt, display, true, true, false)
}
other => {
let Some(action) = other.os_action() else {
unreachable!("local MCP actions handled above")
};
let Some(mcp_dev) = self.mcp_dev.clone() else {
self.pending_mcp_subcommand = Some(other);
self.open_mcp_panel();
return None;
};
let Some(session) = self.os_session.clone() else {
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" /mcp OS actions need /login first"),
);
return None;
};
self.push_line(&Style::new().fg(TN_GRAY).render(&format!(
" ◆ {} → OS MCP Function as a Service {}…",
mcp_dev.name,
action.label()
)));
Some(cmd::cmd(move || async move {
let result = panels::mcp::publish_mcp_to_os(session, mcp_dev, action).await;
Msg::McpOsCompleted(result)
}))
}
}
}
fn execute_skill_subcommand(
&mut self,
subcommand: panels::skill::SkillSubcommand,
) -> Option<Cmd<Msg>> {
match subcommand {
panels::skill::SkillSubcommand::Exit => {
self.exit_skill_dev();
None
}
panels::skill::SkillSubcommand::Clone(url) => {
self.clone_asset_command("skill", url, skill_dir())
}
panels::skill::SkillSubcommand::List(query) => {
self.open_asset_list_panel(os_asset_category_query("skill", &query))
}
panels::skill::SkillSubcommand::Activity(query) => {
let Some(skill_dev) = self.skill_dev.clone() else {
self.pending_skill_subcommand =
Some(panels::skill::SkillSubcommand::Activity(query));
self.open_skill_panel();
return None;
};
self.open_runtime_activity_panel(runtime_asset_query(
"skill",
&skill_dev.name,
&query,
))
}
panels::skill::SkillSubcommand::Review => {
if self.skill_dev.is_none() {
self.pending_skill_subcommand = Some(panels::skill::SkillSubcommand::Review);
self.open_skill_panel();
return None;
}
let skill = self.skill_dev.clone().expect("checked above");
let body = match std::fs::read_to_string(&skill.path) {
Ok(body) => body,
Err(error) => {
self.push_line(&Style::new().fg(TN_RED).render(&format!(
" could not read {}: {error}",
skill.path.display()
)));
return None;
}
};
self.messages
.push(user_bubble("/skill review", self.viewport_content_width()));
self.engage_autonomy(4);
self.review_pending = true;
let prompt = panels::skill::skill_review_prompt(&skill.path, &body);
let display = format!("✦ {} review", skill.name);
self.start_stream_inner(prompt, display, true, true, false)
}
panels::skill::SkillSubcommand::Publish => {
self.execute_skill_os_action(panels::skill::SkillOsAction::Publish)
}
panels::skill::SkillSubcommand::Deploy => {
if self.skill_dev.is_none() {
self.pending_skill_subcommand = Some(panels::skill::SkillSubcommand::Deploy);
self.open_skill_panel();
return None;
}
self.execute_skill_os_action(panels::skill::SkillOsAction::Deploy)
}
panels::skill::SkillSubcommand::Open => {
self.execute_skill_os_action(panels::skill::SkillOsAction::Open)
}
panels::skill::SkillSubcommand::Status => {
self.execute_skill_os_action(panels::skill::SkillOsAction::Status)
}
}
}
fn execute_skill_os_action(
&mut self,
action: panels::skill::SkillOsAction,
) -> Option<Cmd<Msg>> {
let Some(skill_dev) = self.skill_dev.clone() else {
self.pending_skill_subcommand = Some(match action {
panels::skill::SkillOsAction::Publish => panels::skill::SkillSubcommand::Publish,
panels::skill::SkillOsAction::Deploy => panels::skill::SkillSubcommand::Deploy,
panels::skill::SkillOsAction::Open => panels::skill::SkillSubcommand::Open,
panels::skill::SkillOsAction::Status => panels::skill::SkillSubcommand::Status,
});
self.open_skill_panel();
return None;
};
let Some(session) = self.os_session.clone() else {
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" /skill OS actions need /login first"),
);
return None;
};
self.push_line(&Style::new().fg(TN_GRAY).render(&format!(
" ✦ {} → OS skill Function as a Service {}…",
skill_dev.name,
action.label()
)));
Some(cmd::cmd(move || async move {
let result = panels::skill::publish_skill_to_os(session, skill_dev, action).await;
Msg::SkillOsCompleted(result)
}))
}
fn on_submit(&mut self, text: String) -> Option<Cmd<Msg>> {
let trimmed = text.trim();
if trimmed.is_empty() {
return None;
}
if self.compacting.is_some() || self.updating.is_some() {
self.textarea.clear();
return None;
}
if self.shell_mode {
self.shell_mode = false;
let cmd = trimmed.trim_start_matches('!').trim().to_string();
if cmd.is_empty() {
return None;
}
self.messages.push(gutter(
GRADIENT_PREVIEW_END,
&Style::new().bold().render(&format!("! {cmd}")),
));
self.textarea.clear();
self.rebuild_viewport();
return Some(cmd::cmd(move || async move {
let out = tokio::process::Command::new("sh")
.arg("-c")
.arg(&cmd)
.output()
.await;
let text = match out {
Ok(o) => {
let mut s = String::from_utf8_lossy(&o.stdout).into_owned();
s.push_str(&String::from_utf8_lossy(&o.stderr));
if s.trim().is_empty() {
format!("(exit {})", o.status.code().unwrap_or(-1))
} else {
s
}
}
Err(e) => format!("failed to run: {e}"),
};
Msg::ShellOutput(text)
}));
}
if self.research_mode {
self.research_mode = false;
let query = trimmed.trim_start_matches('?').trim().to_string();
if query.is_empty() {
self.textarea.clear();
return None;
}
self.history.push(trimmed.to_string());
self.history_pos = None;
self.textarea.clear();
self.goal = Some(deep_research_goal(&query));
self.goal_since = Some(Instant::now());
self.messages.push(gutter(
TN_CYAN,
&Style::new()
.bold()
.render(&format!("🔬 deep research: {query}")),
));
let os_runtime =
should_use_os_runtime_for_deep_research(&query, self.os_session.is_some());
let runtime_hint = if os_runtime {
" 🎯 goal set · adaptive deep research · OS Runtime fan-out selected (Esc stops)"
} else if self.os_session.is_some() {
" 🎯 goal set · adaptive deep research · local workflow selected (Esc stops)"
} else {
" 🎯 goal set · local deep research · report + HTML artifacts (Esc stops)"
};
self.push_line(&Style::new().fg(TN_GRAY).render(runtime_hint));
let display = format!("🔬 {query}");
self.engage_autonomy(8);
let runtime_expectation = Some(RuntimeExpectation::required("deep research"));
if self.state == State::Idle {
return self.start_deep_research_workflow(query, os_runtime, runtime_expectation);
}
self.seq += 1;
self.queue.push(Queued {
prio: 1,
seq: self.seq,
text: String::new(),
display,
runtime_expectation,
deep_research: Some((query, os_runtime)),
});
self.push_line(&Style::new().fg(TN_GRAY).render(" ⋯ queued"));
self.relayout();
return None;
}
if self.state != State::Idle {
let cmd0 = trimmed.split_whitespace().next().unwrap_or("");
if IDLE_ONLY.contains(&cmd0) {
self.textarea.clear();
self.push_line(&Style::new().fg(TN_YELLOW).render(&format!(
" {cmd0} is unavailable while a turn is running — press Esc to stop first"
)));
return None;
}
}
if let Some(rest) = slash_tail(trimmed, "/login") {
self.textarea.clear();
let Some(os_config) = self.os_config.clone() else {
self.push_line(&format!(
"{}\n{}\n{}\n{}",
Style::new()
.fg(TN_YELLOW)
.render(" /login needs an OS endpoint, but none is configured."),
Style::new().fg(TN_GRAY).render(
" Add it to ~/.a3s/config.acl (or your project's .a3s/config.acl):"
),
Style::new()
.fg(TN_CYAN)
.render(" os = \"https://your-os-host.example.com\""),
Style::new()
.fg(TN_GRAY)
.render(" then restart a3s code and run /login again."),
));
return None;
};
let token = rest.trim();
if !token.is_empty() {
match crate::a3s_os::login_with_token(&os_config, token) {
Ok(session) => {
let label = session.display_label();
crate::a3s_os::export_os_env(&session);
self.os_session = Some(session);
self.refresh_after_auth();
self.push_line(&Style::new().fg(TN_GREEN).render(&format!(
" ✓ signed in to OS as {label} · capabilities skill active"
)));
}
Err(error) => self.push_line(
&Style::new()
.fg(TN_RED)
.render(&format!(" login failed: {error}")),
),
}
return None;
}
if let Some(s) = &self.os_session {
self.push_line(&Style::new().fg(TN_GRAY).render(&format!(
" already signed in to OS as {} · /logout to switch accounts",
s.display_label()
)));
return None;
}
self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" opening OS login in your browser…"),
);
return Some(cmd::cmd(move || async move {
let result = crate::a3s_os::login_via_browser(os_config)
.await
.map(|session| session.display_label())
.map_err(|error| error.to_string());
Msg::OsLogin(result)
}));
}
if trimmed == "/logout" {
self.textarea.clear();
let Some(os_config) = self.os_config.clone() else {
self.push_line(&Style::new().fg(TN_YELLOW).render(
" configure `os = \"https://...\"` in .a3s/config.acl to enable /logout",
));
return None;
};
match crate::a3s_os::logout(&os_config) {
Ok(true) => {
self.os_session = None;
self.asset_list = None;
self.runtime_activity = None;
crate::a3s_os::remove_capability_skill_dir();
crate::a3s_os::clear_os_env();
self.refresh_after_auth();
self.push_line(
&Style::new()
.fg(TN_GREEN)
.render(" ✓ signed out from OS · capabilities skill removed"),
);
}
Ok(false) => {
self.os_session = None;
self.asset_list = None;
self.runtime_activity = None;
crate::a3s_os::remove_capability_skill_dir();
crate::a3s_os::clear_os_env();
self.refresh_after_auth();
self.push_line(&Style::new().fg(TN_GRAY).render(" no OS login was stored"));
}
Err(error) => self.push_line(
&Style::new()
.fg(TN_RED)
.render(&format!(" logout failed: {error}")),
),
}
return None;
}
if let Some(rest) = slash_tail(trimmed, "/ctx") {
return self.handle_ctx_command(rest);
}
if let Some(rest) = slash_tail(trimmed, "/okf") {
return self.handle_okf_command(rest);
}
if let Some(rest) = slash_tail(trimmed, "/kb") {
return self.handle_kb_command(rest);
}
if let Some(rest) = slash_tail(trimmed, "/btw") {
let q = rest.trim().to_string();
self.textarea.clear();
if q.is_empty() {
self.push_line(&Style::new().fg(TN_GRAY).render(" usage: /btw <question>"));
return None;
}
self.btw = Some((q.clone(), None));
let agent = self.agent.clone();
let workspace = self.cwd.clone();
let history = self.session.history();
return Some(cmd::cmd(move || async move {
let conf = a3s_code_core::hitl::ConfirmationPolicy::enabled()
.with_timeout(BACKGROUND_CONFIRM_TIMEOUT_MS, TimeoutAction::Reject);
let sess = match agent.session(workspace, Some(tui_session_options(conf))) {
Ok(s) => s,
Err(e) => return Msg::SideNote(format!("(/btw failed: {e})")),
};
let mut answer = String::new();
if let Ok((mut rx, _join)) = sess.stream(&q, Some(&history)).await {
while let Some(ev) = rx.recv().await {
match ev {
AgentEvent::TextDelta { text } => answer.push_str(&text),
AgentEvent::End { text, .. } => {
if answer.trim().is_empty() {
answer = text;
}
break;
}
_ => {}
}
}
}
Msg::SideNote(answer)
}));
}
if let Some(rest) = slash_tail(trimmed, "/goal") {
let g = rest.trim();
self.textarea.clear();
if g.is_empty() {
match &self.goal {
Some(cur) => self.push_line(&gutter(
TN_CYAN,
&format!("🎯 goal: {cur} (/goal clear to remove)"),
)),
None => self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" usage: /goal <what you're working toward>"),
),
}
} else if g == "clear" {
self.goal = None;
self.goal_since = None;
self.push_line(&Style::new().fg(TN_GRAY).render(" goal cleared"));
return None;
} else {
if let Some(dev) = self.agent_dev.clone() {
let scoped = panels::agent::agent_goal_label(&dev, g);
self.goal = Some(scoped.clone());
self.goal_since = Some(Instant::now());
self.push_line(&gutter(
TN_CYAN,
&format!("🎯 agent goal set: {} · {g}", dev.name),
));
let prompt = panels::agent::agent_dev_prompt(&dev, g);
let display = format!("◇ {} goal: {}", dev.name, truncate(g, 54));
return self.start_stream_inner(prompt, display, true, true, false);
}
self.goal = Some(g.to_string());
self.goal_since = Some(Instant::now());
self.push_line(&gutter(TN_CYAN, &format!("🎯 goal set: {g}")));
return Some(cmd::msg(Msg::Submit(g.to_string())));
}
return None;
}
if let Some(rest) = slash_tail(trimmed, "/loop") {
return self.handle_loop_command(rest);
}
if let Some(rest) = slash_tail(trimmed, "/sleep") {
let focus = rest.trim().to_string();
self.textarea.clear();
self.sleep_pending = true;
self.engage_autonomy(8);
self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" ☾ sleep — consolidating today's work into memory… (Esc stops)"),
);
let directive = panels::sleep::sleep_directive(
&focus,
self.ctx_ready,
&panels::sleep::sleep_today(),
);
let display = if focus.is_empty() {
"☾ sleep".to_string()
} else {
format!("☾ sleep · {focus}")
};
return self.start_stream_inner(directive, display, true, true, false);
}
if let Some(rest) = slash_tail(trimmed, "/flow") {
let description = rest.trim().to_string();
self.textarea.clear();
if let Some(parsed) = panels::flow::parse_flow_subcommand(&description) {
match parsed {
Ok(panels::flow::FlowSubcommand::Clone(url)) => {
return self.clone_asset_command("workflow", url, flow_dir());
}
Ok(panels::flow::FlowSubcommand::List(query)) => {
return self
.open_asset_list_panel(os_asset_category_query("workflow", &query));
}
Ok(panels::flow::FlowSubcommand::Activity(query)) => {
if self.os_session.is_none() {
self.push_line(&os_required_alert(
"workflow runtime activity",
self.os_config.is_some(),
));
} else {
self.pending_flow_subcommand =
Some(panels::flow::FlowSubcommand::Activity(query));
self.open_flow_panel();
}
return None;
}
Ok(panels::flow::FlowSubcommand::Review(target)) => {
let root = flow_dir();
let flows = panels::flow::list_flows(&root);
let picked = match target {
Some(target) => flows
.into_iter()
.find(|flow| flow == &target || flow.ends_with(&target)),
None if flows.len() == 1 => flows.into_iter().next(),
None => None,
};
let Some(file) = picked else {
self.pending_flow_subcommand =
Some(panels::flow::FlowSubcommand::Review(None));
self.open_flow_panel();
return None;
};
let path = root.join(&file);
let design = match std::fs::read_to_string(&path) {
Ok(value) => value,
Err(error) => {
self.push_line(&Style::new().fg(TN_RED).render(&format!(
" could not read {}: {error}",
path.display()
)));
return None;
}
};
if serde_json::from_str::<serde_json::Value>(&design).is_err() {
self.push_line(
&Style::new()
.fg(TN_RED)
.render(&format!(" {} is not valid JSON", file)),
);
return None;
}
self.messages.push(user_bubble(
&format!("/flow review {file}"),
self.viewport_content_width(),
));
self.engage_autonomy(4);
self.review_pending = true;
let prompt = panels::flow::flow_review_prompt(&path, &design);
let display = format!("⧉ flow review: {}", truncate(&file, 48));
return self.start_stream_inner(prompt, display, true, true, false);
}
Ok(action @ panels::flow::FlowSubcommand::Publish)
| Ok(action @ panels::flow::FlowSubcommand::Run)
| Ok(action @ panels::flow::FlowSubcommand::Deploy)
| Ok(action @ panels::flow::FlowSubcommand::Open)
| Ok(action @ panels::flow::FlowSubcommand::Logs)
| Ok(action @ panels::flow::FlowSubcommand::Status) => {
if self.os_session.is_none() {
self.push_line(
&Style::new().fg(TN_YELLOW).render(
" /flow publish/run/deploy/open/logs/status needs OS — sign in with /login first",
),
);
} else {
self.pending_flow_subcommand = Some(action);
self.open_flow_panel();
}
return None;
}
Err(e) => {
self.push_line(&Style::new().fg(TN_RED).render(&format!(" {e}")));
return None;
}
}
}
if description.is_empty() {
if self.os_session.is_none() {
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" /flow needs OS — sign in with /login first"),
);
} else {
self.open_flow_panel();
}
return None;
}
let dir = flow_dir();
self.push_line(&Style::new().fg(TN_GRAY).render(&format!(
" ⧉ drafting a flow DAG → {} (then /flow opens it in the designer)",
dir.display()
)));
self.engage_autonomy(8);
let prompt = panels::flow::flow_gen_prompt(&description, &dir.to_string_lossy());
let display = format!("⧉ flow: {}", truncate(&description, 60));
return self.start_stream_inner(prompt, display, true, true, false);
}
if let Some(rest) = slash_tail(trimmed, "/agent") {
let description = rest.trim().to_string();
self.textarea.clear();
if let Some(parsed) = panels::agent::parse_agent_subcommand(&description) {
return match parsed {
Ok(subcommand) => self.execute_agent_subcommand(subcommand),
Err(e) => {
self.push_line(&Style::new().fg(TN_RED).render(&format!(" {e}")));
None
}
};
}
if description.is_empty() {
self.open_agent_panel();
return None;
}
let dir = agent_dir();
self.push_line(&Style::new().fg(TN_GRAY).render(&format!(
" ◇ drafting an agent definition → {} (then /agent starts local dev)",
dir.display()
)));
self.engage_autonomy(8);
let prompt = panels::agent::agent_gen_prompt(&description, &dir.to_string_lossy());
let display = format!("◇ agent: {}", truncate(&description, 60));
return self.start_stream_inner(prompt, display, true, true, false);
}
if let Some(rest) = slash_tail(trimmed, "/mcp") {
let description = rest.trim().to_string();
self.textarea.clear();
if let Some(parsed) = panels::mcp::parse_mcp_subcommand(&description) {
return match parsed {
Ok(subcommand) => self.execute_mcp_subcommand(subcommand),
Err(e) => {
self.push_line(&Style::new().fg(TN_RED).render(&format!(" {e}")));
None
}
};
}
if description.is_empty() {
self.open_mcp_panel();
return None;
}
let dir = mcp_dir();
self.push_line(&Style::new().fg(TN_GRAY).render(&format!(
" ◆ drafting an MCP server asset → {} (then /mcp starts local dev)",
dir.display()
)));
self.engage_autonomy(8);
let prompt = panels::mcp::mcp_gen_prompt(&description, &dir.to_string_lossy());
let display = format!("◆ mcp: {}", truncate(&description, 60));
return self.start_stream_inner(prompt, display, true, true, false);
}
if let Some(rest) = slash_tail(trimmed, "/skill") {
let description = rest.trim().to_string();
self.textarea.clear();
if let Some(parsed) = panels::skill::parse_skill_subcommand(&description) {
return match parsed {
Ok(subcommand) => self.execute_skill_subcommand(subcommand),
Err(e) => {
self.push_line(&Style::new().fg(TN_RED).render(&format!(" {e}")));
None
}
};
}
if description.is_empty() {
self.open_skill_panel();
return None;
}
let dir = skill_dir();
self.push_line(&Style::new().fg(TN_GRAY).render(&format!(
" ✦ drafting a skill asset → {} (then /skill starts local dev)",
dir.display()
)));
self.engage_autonomy(8);
let prompt = panels::skill::skill_gen_prompt(&description, &dir.to_string_lossy());
let display = format!("✦ skill: {}", truncate(&description, 60));
return self.start_stream_inner(prompt, display, true, true, false);
}
match trimmed {
"/exit" => return Some(cmd::quit()),
"/fork" => {
self.textarea.clear();
let store = self.store.clone();
let src = self.session_id.clone();
let dst = new_session_id();
return Some(cmd::cmd(move || async move {
match store.load(&src).await {
Ok(Some(mut data)) => {
data.id = dst.clone();
match store.save(&data).await {
Ok(()) => Msg::Forked(Ok(dst)),
Err(e) => Msg::Forked(Err(format!("could not save the fork: {e}"))),
}
}
Ok(None) => Msg::Forked(Err(
"nothing to fork yet — start a conversation first".into(),
)),
Err(e) => Msg::Forked(Err(format!("could not read the session: {e}"))),
}
}));
}
"/clear" => {
self.messages.clear();
self.plan.clear();
self.runtime.clear_turn_entities();
self.queue.clear();
self.completed = 0;
self.textarea.clear();
self.review_pending = false;
self.sleep_pending = false;
self.restore_autonomy();
self.pending_ctx = None;
self.ctx_hits.clear();
self.agent_dev = None;
self.pending_flow_subcommand = None;
self.pending_agent_subcommand = None;
self.mcp_dev = None;
self.pending_mcp_subcommand = None;
self.skill_dev = None;
self.pending_skill_subcommand = None;
self.okf_picker = None;
self.pending_okf_subcommand = None;
self.okf_dev = None;
self.asset_list = None;
self.runtime_activity = None;
self.kb = None;
let prev_id = std::mem::replace(&mut self.session_id, new_session_id());
let model = self.model.clone();
match self.rebuild_session(model.as_deref()) {
Ok((s, _)) => {
self.replace_session(s);
self.compact_summary = None;
self.output_tokens = 0;
self.last_prompt_tokens = 0;
self.ctx_warned_tier = 0; }
Err(_) => self.session_id = prev_id,
}
self.relayout();
self.rebuild_viewport();
return None;
}
"/init" => {
self.textarea.clear();
self.messages.push(user_bubble(
"/init — generate AGENTS.md",
self.viewport_content_width(),
));
self.rebuild_viewport();
return self.start_stream(
"Analyze this codebase and create (or update) an AGENTS.md file at the \
project root. Include: a concise project overview, the exact build / test / \
lint / run commands, the high-level architecture and key directories, and \
the conventions an AI coding agent should follow. Base everything on what's \
actually in the workspace, and write the file with your file-writing tool."
.to_string(),
);
}
"/compact" => {
self.textarea.clear();
if self.state != State::Idle {
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" finish the current turn before compacting"),
);
return None;
}
let history = self.session.history();
if history.is_empty() {
self.push_line(&Style::new().fg(TN_GRAY).render(" nothing to compact yet"));
return None;
}
self.compacting = Some(Instant::now()); let agent = self.agent.clone();
let workspace = self.cwd.clone();
let prompt = match &self.compact_summary {
Some(prev) => format!(
"An earlier part of this conversation was already condensed into this \
summary:\n\n{prev}\n\nProduce a SINGLE updated summary that fully \
incorporates the summary above AND the conversation history below, so a \
fresh session can continue seamlessly: the goal, key decisions, \
files/commands touched, current state, and the immediate next steps. Be \
thorough but compact."
),
None => "Summarize this conversation so a fresh session can continue \
seamlessly: the goal, key decisions, files/commands touched, current \
state, and the immediate next steps. Be thorough but compact."
.to_string(),
};
return Some(cmd::cmd(move || async move {
let conf = a3s_code_core::hitl::ConfirmationPolicy::enabled()
.with_timeout(BACKGROUND_CONFIRM_TIMEOUT_MS, TimeoutAction::Reject);
let mut summary = String::new();
if let Ok(sess) = agent.session(workspace, Some(tui_session_options(conf))) {
if let Ok((mut rx, _j)) = sess.stream(&prompt, Some(&history)).await {
while let Some(ev) = rx.recv().await {
match ev {
AgentEvent::TextDelta { text } => summary.push_str(&text),
AgentEvent::End { text, .. } => {
if summary.trim().is_empty() {
summary = text;
}
break;
}
_ => {}
}
}
}
}
Msg::Compacted(summary)
}));
}
"/help" => {
self.textarea.clear();
self.help_open = true;
self.help_scroll = 0;
return None;
}
"/view" => {
self.textarea.clear();
if let Some(spec) = self.last_view.clone() {
self.open_remote_view(&spec);
} else {
self.push_line(&Style::new().fg(TN_GRAY).render(
" no OS view yet — run an OS query that returns a viewUrl, then /view",
));
}
return None;
}
"/auto" => {
self.mode = Mode::Auto;
self.textarea.clear();
self.rebuild_viewport();
return None;
}
"/config" => {
self.textarea.clear();
let path = find_config().map(std::path::PathBuf::from).or_else(|| {
let p = default_config_path()?;
let _ = write_template_config(&p);
Some(p)
});
match path {
Some(p) => self.open_config_in_ide(&p),
None => self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" could not locate a home directory for ~/.a3s/config.acl"),
),
}
return None;
}
"/model" => {
self.textarea.clear();
if let Some(s) = self.os_session.clone() {
let need_fetch = self.os_gateway_models.as_ref().is_none_or(|m| m.is_empty());
if need_fetch {
let (addr, token) = (s.address.clone(), s.access_token.clone());
return Some(cmd::cmd(move || async move {
Msg::OsGatewayModels(
crate::a3s_os::fetch_gateway_models(&addr, &token).await,
)
}));
}
}
self.open_model_menu();
return None;
}
"/effort" => {
self.textarea.clear();
self.effort_panel = Some(self.effort);
return None;
}
"/top" => {
self.textarea.clear();
self.top = Some(Vec::new());
self.top_history.clear();
self.top_scroll = 0;
self.top_sel = 0;
self.top_focus = None;
return Some(cmd::cmd(|| async { Msg::TopData(fetch_top().await) }));
}
"/ide" => {
self.textarea.clear();
let entries = ide_children(std::path::Path::new(&self.cwd), 0);
self.ide = Some(Ide::browse(entries, "workspace"));
return None;
}
"/plugin" => {
self.textarea.clear();
if self.skills.is_empty() {
self.push_line(&Style::new().fg(TN_GRAY).render(
" no skills/plugins found (~/.claude/skills, ~/.codex/skills, ~/.claude/plugins)",
));
} else {
self.plugins_panel = Some(0);
}
return None;
}
"/theme" => {
self.textarea.clear();
let cur = SYNTAX_THEME.load(std::sync::atomic::Ordering::Relaxed);
self.theme_panel = Some(cur.min(THEMES.len() - 1));
return None;
}
"/output" => {
self.textarea.clear();
match self.format_tool_log() {
Some(content) => self.open_readonly_in_ide("tool-calls.txt", &content),
None => self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" no tool calls yet this session"),
),
}
return None;
}
"/reload" => {
self.textarea.clear();
let dirs = agent_skill_dirs(&self.cwd);
self.skills = load_skills(&dirs);
self.skill_count = count_skill_files(&dirs);
let model = self.model.clone();
match self.rebuild_session(model.as_deref()) {
Ok((session, _)) => {
self.replace_session(session);
self.push_line(&Style::new().fg(TN_GREEN).render(&format!(
" ↻ reloaded — {} skills available",
self.skills.len()
)));
}
Err(error) => {
self.push_line(
&Style::new()
.fg(TN_RED)
.render(&format!(" reload failed: {error}")),
);
}
}
return None;
}
"/update" => {
self.textarea.clear();
self.updating = Some(Instant::now()); self.relayout();
return Some(cmd::cmd(|| async {
let latest = tokio::task::spawn_blocking(crate::update::fetch_latest)
.await
.ok()
.flatten();
Msg::UpdatePlan(latest)
}));
}
"/memory" => {
self.textarea.clear();
let dir = memory_dir();
self.memory = Some(MemPanel {
entries: Vec::new(),
sel: 0,
details: std::collections::BTreeMap::new(),
graph: MemoryGraph::default(),
loaded_from_session: false,
detail: memutil::MemDetail::default(),
detail_scroll: 0,
dir: dir.clone(),
note: "loading…".into(),
});
return Some(self.load_memory_panel(dir));
}
_ => {}
}
self.history.push(trimmed.to_string());
self.history_pos = None;
self.messages
.push(user_bubble(trimmed, self.viewport_content_width()));
self.textarea.clear();
let loop_cont = std::mem::take(&mut self.loop_continuation);
let prompt = match (loop_cont, self.pending_ctx.take()) {
(false, Some(c)) => format!("{c}\n\n{trimmed}"),
_ => trimmed.to_string(),
};
let (prompt, display) = match &self.agent_dev {
Some(dev) => (
panels::agent::agent_dev_prompt(dev, &prompt),
format!("◇ {}: {}", dev.name, truncate(trimmed, 60)),
),
None => match &self.mcp_dev {
Some(dev) => (
panels::mcp::mcp_dev_prompt(dev, &prompt),
format!("◆ {}: {}", dev.name, truncate(trimmed, 60)),
),
None => match &self.skill_dev {
Some(dev) => (
panels::skill::skill_dev_prompt(dev, &prompt),
format!("✦ {}: {}", dev.name, truncate(trimmed, 60)),
),
None => match &self.okf_dev {
Some(dev) => (
panels::okf::okf_dev_prompt(dev, &prompt),
format!("⌁ {}: {}", dev.name, truncate(trimmed, 60)),
),
None => (prompt, trimmed.to_string()),
},
},
},
};
if self.state == State::Idle {
self.start_stream_inner(prompt, display, true, true, false)
} else {
self.seq += 1;
self.queue.push(Queued {
prio: 1,
seq: self.seq,
text: prompt,
display,
runtime_expectation: None,
deep_research: None,
});
self.push_line(&Style::new().fg(TN_GRAY).render(" ⋯ queued"));
self.relayout();
None
}
}
fn paste_clipboard_image(&mut self) {
let dest =
std::env::temp_dir().join(format!("a3s-paste-{}.png", self.pending_images.len()));
if !clipboard_image_to(&dest) {
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" no image in clipboard (Ctrl+V pastes a copied/screenshot image)"),
);
return;
}
let Ok(bytes) = std::fs::read(&dest) else {
return;
};
self.messages.push(gutter(
ACCENT,
"📎 pasted image (sends with your next message):",
));
let cols = self.transcript_markdown_width().min(72);
if let Some(lines) = render_image_file(&dest, cols, 16) {
for l in lines {
self.messages.push(format!("{}{l}", " ".repeat(PAD)));
}
}
self.rebuild_viewport();
self.pending_images
.push(a3s_code_core::llm::Attachment::png(bytes));
}
fn start_stream(&mut self, prompt: String) -> Option<Cmd<Msg>> {
self.start_stream_inner(prompt.clone(), prompt, true, true, false)
}
fn start_ultracode_synthesis(
&mut self,
prompt: String,
display_task: String,
) -> Option<Cmd<Msg>> {
self.ultracode_synthesis_used = true;
self.push_line(&Style::new().fg(TN_GRAY).render(" ⇉ synthesizing results…"));
self.start_stream_inner(prompt, display_task, false, false, true)
}
fn start_deep_research_workflow(
&mut self,
query: String,
os_runtime: bool,
runtime_expectation: Option<RuntimeExpectation>,
) -> Option<Cmd<Msg>> {
self.streaming.clear();
self.got_delta = false;
self.turn_text.clear();
self.turn_had_agent_activity = false;
self.turn_text_after_activity = false;
if let Some(expectation) = runtime_expectation {
self.runtime_expectation = Some(expectation);
}
self.ultracode_synthesis_inflight = false;
self.ultracode_synthesis_used = false;
self.last_paint = None;
self.viewport.set_auto_scroll(true);
self.plan.clear();
self.runtime.clear_turn_entities();
self.running_task = Some(format!("🔬 {query}"));
self.state = State::Streaming;
self.relayout();
self.stream_started = Some(Instant::now());
self.spinner.start();
self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" ⇉ gathering evidence with DynamicWorkflowRuntime…"),
);
self.rebuild_viewport();
let args = deep_research_workflow_args(&query, os_runtime);
let (progress_rx, workflow_join) = self
.session
.tool_with_events("dynamic_workflow", args.clone());
let progress_rx = Arc::new(Mutex::new(progress_rx));
self.rx = Some(progress_rx.clone());
self.stream_join = None;
self.host_progress_inflight = true;
self.interrupting = false;
Some(cmd::batch(vec![
cmd::cmd(move || async move {
let result = match workflow_join.await {
Ok(result) => result.map_err(|err| err.to_string()),
Err(err) => Err(err.to_string()),
};
Msg::DeepResearchWorkflowCompleted {
query,
os_runtime,
args,
result,
}
}),
pump(progress_rx),
spinner_tick(),
]))
}
fn on_deep_research_workflow_completed(
&mut self,
query: String,
os_runtime: bool,
args: serde_json::Value,
result: Result<ToolCallResult, String>,
) -> Option<Cmd<Msg>> {
if self.state != State::Streaming {
return None;
}
self.host_progress_inflight = false;
self.rx = None;
let (output, exit_code, metadata) = match result {
Ok(result) => (result.output, result.exit_code, result.metadata),
Err(error) => (error, 1, None),
};
let tool_id = format!("host-dynamic-workflow-{}", self.seq);
self.runtime
.start_tool(tool_id.clone(), "dynamic_workflow".to_string());
self.runtime
.push_tool_input(&serde_json::to_string(&args).unwrap_or_default());
let completed = self.runtime.end_tool(
&tool_id,
"dynamic_workflow".to_string(),
output.clone(),
exit_code,
);
self.push_line(&render_tool_end(
"dynamic_workflow",
exit_code,
&output,
metadata.as_ref(),
completed.args.as_ref(),
self.viewport_content_width(),
));
self.record_runtime_tool_evidence("dynamic_workflow");
if metadata
.as_ref()
.is_some_and(|value| json_contains_tool_evidence(value, "runtime"))
{
self.record_runtime_tool_evidence("runtime");
}
if metadata
.as_ref()
.is_some_and(|value| json_contains_tool_evidence(value, "parallel_task"))
{
self.record_runtime_parallel_evidence();
}
self.capture_workflow("dynamic_workflow", completed.args.as_ref());
if let Some(spec) = self.find_remote_view_spec(&output) {
self.remember_remote_view(spec);
}
let prompt = if exit_code == 0 {
deep_research_synthesis_prompt(&query, os_runtime, &output, metadata.as_ref())
} else {
self.push_line(
&Style::new()
.fg(TN_YELLOW)
.render(" ⚠ dynamic workflow failed; starting recovery synthesis…"),
);
deep_research_recovery_prompt(&query, os_runtime, &output, metadata.as_ref())
};
self.start_stream_inner_with_runtime(
prompt,
format!("🔬 synthesize {query}"),
false,
false,
false,
None,
)
}
fn start_stream_inner(
&mut self,
prompt: String,
display_task: String,
clear_turn_artifacts: bool,
include_attachments: bool,
synthesis: bool,
) -> Option<Cmd<Msg>> {
self.start_stream_inner_with_runtime(
prompt,
display_task,
clear_turn_artifacts,
include_attachments,
synthesis,
None,
)
}
fn start_stream_inner_with_runtime(
&mut self,
prompt: String,
display_task: String,
clear_turn_artifacts: bool,
include_attachments: bool,
synthesis: bool,
runtime_expectation: Option<RuntimeExpectation>,
) -> Option<Cmd<Msg>> {
self.streaming.clear();
self.got_delta = false; self.turn_text.clear();
self.turn_had_agent_activity = false;
self.turn_text_after_activity = false;
if let Some(expectation) = runtime_expectation {
self.runtime_expectation = Some(expectation);
}
self.ultracode_synthesis_inflight = synthesis;
if !synthesis {
self.ultracode_synthesis_used = false;
}
self.last_paint = None; self.viewport.set_auto_scroll(true); if clear_turn_artifacts {
self.plan.clear(); self.runtime.clear_turn_entities();
} else {
self.runtime.clear_live_tools();
}
self.running_task = Some(display_task);
self.state = State::Streaming;
self.relayout();
self.stream_started = Some(Instant::now());
self.spinner.start();
self.rebuild_viewport();
let session = self.session.clone();
let atts = if include_attachments {
std::mem::take(&mut self.pending_images)
} else {
Vec::new()
};
let prompt = match &self.goal {
Some(g) => format!("[Ongoing goal: {g}]\n\n{prompt}"),
None => prompt,
};
Some(cmd::batch(vec![
cmd::cmd(move || async move {
let res = if atts.is_empty() {
session.stream(prompt.as_str(), None).await
} else {
session
.stream_with_attachments(prompt.as_str(), &atts, None)
.await
};
match res {
Ok((rx, join)) => Msg::StreamStarted(Arc::new(Mutex::new(rx)), join),
Err(e) => Msg::StreamError(e.to_string()),
}
}),
spinner_tick(),
]))
}
fn drain_queue(&mut self) -> Option<Cmd<Msg>> {
let next = self.queue.pop()?;
if let Some((query, os_runtime)) = next.deep_research {
return self.start_deep_research_workflow(query, os_runtime, next.runtime_expectation);
}
self.start_stream_inner_with_runtime(
next.text,
next.display,
true,
true,
false,
next.runtime_expectation,
)
}
fn complete_turn(&mut self) -> Option<Cmd<Msg>> {
if self.state == State::Streaming {
self.completed += 1;
}
self.warn_missing_runtime_evidence();
let synthesis = self.prepare_ultracode_synthesis();
self.finish();
if let Some((prompt, display_task)) = synthesis {
return self.start_ultracode_synthesis(prompt, display_task);
}
if self.loop_remaining > 0 && self.queue.is_empty() {
if let Some(prompt) = self
.runtime_expectation
.as_ref()
.and_then(RuntimeExpectation::corrective_prompt)
{
self.loop_remaining -= 1;
let n = self.loop_remaining;
self.push_line(&Style::new().fg(TN_GRAY).render(&format!(
" ↻ runtime evidence retry ({n} left · Esc to stop)"
)));
self.loop_continuation = true;
return Some(cmd::msg(Msg::Submit(prompt)));
}
}
if self.loop_remaining > 0 && self.queue.is_empty() {
self.loop_remaining -= 1;
let n = self.loop_remaining;
self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(&format!(" ↻ loop ({n} left · Esc to stop)")),
);
self.loop_continuation = true;
return Some(cmd::msg(Msg::Submit(
"Continue. If the task is fully complete, reply DONE and stop.".to_string(),
)));
}
if self.loop_remaining == 0 {
self.restore_autonomy();
}
self.drain_queue()
}
fn resume_after_pending_confirmation(&self) -> Cmd<Msg> {
resume_after_pending_confirmation_cmd(self.rx.clone())
}
fn engage_autonomy(&mut self, budget: usize) {
self.loop_remaining = self.loop_remaining.max(budget);
if self.mode != Mode::Auto {
self.autonomy_restore = Some(self.mode);
self.mode = Mode::Auto;
self.push_line(&Style::new().fg(TN_GRAY).render(
" ⏵⏵ auto mode engaged for this task — restores when it completes (Esc stops)",
));
}
}
fn record_runtime_tool_evidence(&mut self, name: &str) {
if let Some(expectation) = &mut self.runtime_expectation {
expectation.record_tool(name);
}
}
fn record_runtime_parallel_evidence(&mut self) {
if let Some(expectation) = &mut self.runtime_expectation {
expectation.record_parallel_work();
}
}
fn record_runtime_view_evidence(&mut self) {
if let Some(expectation) = &mut self.runtime_expectation {
expectation.record_remote_view();
}
}
fn warn_missing_runtime_evidence(&mut self) {
let warning = self
.runtime_expectation
.as_mut()
.and_then(RuntimeExpectation::missing_warning);
if let Some(warning) = warning {
self.push_line(&Style::new().fg(TN_YELLOW).render(&warning));
}
}
fn restore_autonomy(&mut self) {
self.runtime_expectation = None;
if let Some(prev) = self.autonomy_restore.take() {
self.mode = prev;
self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(" ⏵ autonomous task ended — auto mode restored to your previous mode"),
);
}
}
fn on_agent_event(&mut self, event: AgentEvent) -> Option<Cmd<Msg>> {
self.rx.as_ref()?;
match event {
AgentEvent::TextDelta { text } => {
self.mark_assistant_text(&text);
self.got_delta = true;
self.turn_text.push_str(&text);
self.streaming.push(&text);
self.update_viewport_with_stream();
}
AgentEvent::ReasoningDelta { text } => {
self.thinking.push_str(&text);
self.update_viewport_with_stream();
}
AgentEvent::ToolStart { id, name } => {
self.mark_agent_activity();
self.finalize_streaming();
self.runtime.start_tool(id, name);
}
AgentEvent::ToolInputDelta { delta } => {
self.runtime.push_tool_input(&delta);
}
AgentEvent::ToolOutputDelta { id, name, delta } => {
self.runtime.push_tool_output(id, name, &delta);
if let Some(output) = self
.runtime
.live_tool()
.map(|tool| tool.output().to_string())
{
if let Some(spec) = self.find_remote_view_spec(&output) {
self.remember_remote_view(spec);
}
}
self.update_viewport_with_stream();
}
AgentEvent::ToolEnd {
id,
name,
output,
exit_code,
metadata,
..
} => {
self.mark_agent_activity();
let completed = self
.runtime
.end_tool(&id, name.clone(), output.clone(), exit_code);
self.push_line(&render_tool_end(
&name,
exit_code,
&output,
metadata.as_ref(),
completed.args.as_ref(),
self.viewport_content_width(),
));
self.record_runtime_tool_evidence(&name);
self.capture_workflow(&name, completed.args.as_ref());
if let Some(spec) = self.find_remote_view_spec(&output) {
self.remember_remote_view(spec);
}
}
AgentEvent::SubagentStart {
task_id,
agent,
description,
started_ms,
..
} => {
self.mark_agent_activity();
self.finalize_streaming();
self.record_runtime_parallel_evidence();
self.runtime.start_subagent(
task_id,
agent,
description,
instant_from_epoch_ms(started_ms),
);
self.relayout();
}
AgentEvent::SubagentProgress {
task_id, metadata, ..
} => {
self.mark_agent_activity();
let toks = metadata
.get("completion_tokens")
.or_else(|| metadata.pointer("/usage/completion_tokens"))
.and_then(|v| v.as_u64());
if let Some(t) = toks {
self.runtime.add_subagent_tokens(&task_id, t);
}
}
AgentEvent::SubagentEnd {
task_id,
agent,
output,
success,
finished_ms,
..
} => {
self.mark_agent_activity();
self.runtime.end_subagent(
task_id,
agent.clone(),
success,
instant_from_epoch_ms(finished_ms),
);
self.relayout();
let (mark, color) = if success {
("✓", TN_GREEN)
} else {
("✗", TN_RED)
};
let snippet = output.lines().next().unwrap_or("").trim();
let snippet = truncate(snippet, self.width.saturating_sub(20) as usize);
self.push_line(&Style::new().fg(color).render(&format!(
" ⇉ {mark} {agent}{}",
if snippet.is_empty() {
String::new()
} else {
format!(" · {snippet}")
}
)));
}
AgentEvent::ContextCompacted {
before_messages,
after_messages,
percent_before,
..
} => {
if after_messages < before_messages {
let pct = if self.context_limit > 0 {
(percent_before * CORE_MAX_CONTEXT_TOKENS * 100.0
/ self.context_limit as f32)
.round()
.min(100.0) as u32
} else {
(percent_before * 100.0).round() as u32
};
self.push_line(&Style::new().fg(TN_GRAY).italic().render(&format!(
" ✦ context auto-compacted at {pct}% · {before_messages} → {after_messages} messages"
)));
}
}
AgentEvent::ConfirmationRequired {
tool_id,
tool_name,
args,
..
} => {
if self.mode.auto_approves(&tool_name) {
let session = self.session.clone();
return Some(cmd::cmd(move || async move {
let _ = session.confirm_tool_use(&tool_id, true, None).await;
Msg::Resume
}));
}
self.state = State::Awaiting;
self.runtime.remove_tool(&tool_id);
self.approval_sel = 0;
let label = tool_label(&tool_name, Some(&args));
self.pending_tool = Some((tool_id, label));
return self.rx.clone().map(pump);
}
AgentEvent::ConfirmationReceived {
tool_id,
approved,
reason,
} => {
if let Some(label) = take_pending_tool_label(&mut self.pending_tool, &tool_id) {
self.state = State::Streaming;
if !approved {
let suffix = reason
.filter(|value| !value.trim().is_empty())
.map(|value| format!(" · {value}"))
.unwrap_or_default();
self.push_line(
&Style::new()
.fg(TN_RED)
.render(&format!(" ⎿ denied {label}{suffix}")),
);
}
return Some(self.resume_after_pending_confirmation());
}
}
AgentEvent::ConfirmationTimeout {
tool_id,
action_taken,
} => {
if let Some(label) = take_pending_tool_label(&mut self.pending_tool, &tool_id) {
self.state = State::Streaming;
let (color, note) = if action_taken == "auto_approved" {
(
TN_YELLOW,
format!(" ⎿ confirmation timed out · auto-approved {label}"),
)
} else {
(
TN_RED,
format!(" ⎿ confirmation timed out · denied {label}"),
)
};
self.push_line(&Style::new().fg(color).render(¬e));
return Some(self.resume_after_pending_confirmation());
}
}
AgentEvent::PermissionDenied { tool_id, .. } => {
self.runtime.remove_tool(&tool_id);
}
AgentEvent::TurnEnd { usage, .. } => {
if usage.prompt_tokens > 0 {
self.last_prompt_tokens = usage.prompt_tokens;
self.maybe_warn_ctx();
}
}
AgentEvent::End {
text, usage, meta, ..
} => {
if self.loop_remaining > 0 && !self.sleep_pending {
let r = if text.is_empty() {
self.streaming.raw_content().to_string()
} else {
text.clone()
};
if r.split(|c: char| !c.is_alphabetic())
.any(|w| w.eq_ignore_ascii_case("done"))
{
self.loop_remaining = 0;
}
}
let review_text = if text.is_empty() {
self.turn_text.clone()
} else {
text.clone()
};
if !self.got_delta && !text.is_empty() {
self.mark_assistant_text(&text);
self.streaming.push(&text);
}
self.finalize_streaming();
self.capture_review(&review_text);
let sleep_save = self.capture_sleep(&review_text);
self.disarm_sleep_if_over(sleep_save.is_some());
self.output_tokens += if usage.completion_tokens > 0 {
usage.completion_tokens
} else {
usage.total_tokens.saturating_sub(usage.prompt_tokens)
};
if self.model.is_none() {
self.model = meta.and_then(|m| m.response_model.or(m.request_model));
}
return match (sleep_save, self.complete_turn()) {
(Some(save), Some(next)) => Some(cmd::batch(vec![save, next])),
(save, next) => save.or(next),
};
}
AgentEvent::Error { message } => {
self.push_line(
&Style::new()
.fg(TN_RED)
.render(&format!(" error: {message}")),
);
self.loop_remaining = 0; self.review_pending = false; self.sleep_pending = false; self.restore_autonomy();
self.finish();
return self.drain_queue();
}
AgentEvent::PlanningEnd { plan, .. } => {
self.mark_agent_activity();
self.set_plan(&plan.steps);
}
AgentEvent::TaskUpdated { tasks, .. } => {
self.mark_agent_activity();
self.set_plan(&tasks);
}
AgentEvent::StepStart { step_id, .. } => {
self.mark_agent_activity();
self.set_task_status(&step_id, '▶', TN_YELLOW);
}
AgentEvent::StepEnd {
step_id, status, ..
} => {
self.mark_agent_activity();
let (g, c) = task_status_style(status);
self.set_task_status(&step_id, g, c);
}
_ => {}
}
self.rx.clone().map(pump)
}
fn maybe_warn_ctx(&mut self) {
if self.context_limit == 0 {
return;
}
let pct = (self.last_prompt_tokens * 100 / self.context_limit as usize).min(100);
let (latch, warn) = ctx_warn_tier(pct, self.ctx_warned_tier);
self.ctx_warned_tier = latch;
if warn.is_some() {
self.finalize_streaming();
}
match warn {
Some(85) => self.push_line(&Style::new().fg(TN_RED).render(&format!(
" ✦ context {pct}% full — auto-compacting soon; /compact to summarize now"
))),
Some(_) => self.push_line(&Style::new().fg(TN_YELLOW).render(&format!(
" ✦ context {pct}% full — auto-compacts near 85%; /compact to summarize early"
))),
None => {}
}
}
fn mark_agent_activity(&mut self) {
self.turn_had_agent_activity = true;
self.turn_text_after_activity = false;
}
fn mark_assistant_text(&mut self, text: &str) {
if !text.trim().is_empty() {
self.turn_text_after_activity = true;
}
}
fn prepare_ultracode_synthesis(&self) -> Option<(String, String)> {
if !needs_synthesis(
self.ultracode_synthesis_inflight,
self.ultracode_synthesis_used,
self.turn_had_agent_activity,
self.turn_text_after_activity,
) {
return None;
}
let user_task = self
.running_task
.as_deref()
.filter(|task| !task.trim().is_empty())
.unwrap_or("the previous task");
let mut prompt = format!(
"[synthesis]\n\
The previous turn completed planning/tool/subagent work \
but stopped without a final user-facing answer.\n\n\
Original user task:\n{user_task}\n\n\
Write the final answer now in the user's language. Synthesize the \
completed work into a useful response. Do not call tools or start \
more subagents unless it is strictly necessary to avoid an incorrect \
answer. If a child run produced no text output, summarize the \
available plan/status instead of exposing raw task metadata.\n"
);
if !self.plan.is_empty() {
prompt.push_str("\nPlan/status:\n");
for (_, text, glyph, _) in &self.plan {
let status = match glyph {
'✔' => "done",
'▶' => "in progress",
'✗' => "failed",
_ => "pending",
};
prompt.push_str(&format!("- [{status}] {text}\n"));
}
}
let subagents = self.runtime.subagents();
if !subagents.is_empty() {
prompt.push_str("\nSubagents:\n");
for agent in subagents {
let status = match agent.success {
Some(true) => "done",
Some(false) => "failed",
None if agent.done => "done",
None => "unknown",
};
prompt.push_str(&format!(
"- [{status}] {}: {}\n",
agent.agent, agent.description
));
}
}
if let Some(workflow) = &self.last_workflow {
prompt.push_str("\nLatest workflow artifact excerpt:\n");
prompt.push_str(&truncate(workflow, 4000));
prompt.push('\n');
}
Some((prompt, user_task.to_string()))
}
fn finalize_streaming(&mut self) {
let rendered = self.streaming.view();
if !rendered.trim().is_empty() {
let block = gutter(TN_GREEN, &rendered);
let recent_dup = self.messages.iter().rev().take(4).any(|m| m == &block);
if !recent_dup {
self.messages.push(block);
}
}
self.streaming.clear();
self.thinking.clear();
self.rebuild_viewport();
}
fn finish(&mut self) {
self.state = State::Idle;
self.running_task = None;
self.runtime.clear_turn_entities();
self.ultracode_synthesis_inflight = false;
self.relayout();
self.stream_started = None;
self.spinner.stop();
self.rx = None;
self.stream_join = None;
self.host_progress_inflight = false;
self.interrupting = false;
self.rebuild_viewport();
}
fn push_line(&mut self, line: &str) {
self.messages.push(line.to_string());
self.rebuild_viewport();
}
fn open_remote_view(&mut self, spec: &remote_ui::ViewSpec) {
match remote_ui::open_window(spec) {
Ok(remote_ui::OpenedWith::Webview) => {}
Ok(remote_ui::OpenedWith::Browser) => {
let helper = remote_ui::webview_helper_path()
.map(|path| path.display().to_string())
.unwrap_or_else(|| {
"missing; install a3s-webview or set A3S_WEBVIEW_BIN".to_string()
});
self.push_line(&Style::new().fg(TN_YELLOW).render(&format!(
" ↗ opened URL in browser: {} · authenticated RemoteUI popup helper: {helper}",
spec.url
)));
}
Err(err) => {
self.push_line(
&Style::new()
.fg(TN_GRAY)
.render(&format!(" 🔗 open in your browser: {} ({err})", spec.url)),
);
}
}
}
fn find_remote_view_spec(&self, output: &str) -> Option<remote_ui::ViewSpec> {
let os_origin = self
.os_session
.as_ref()
.map(|s| crate::a3s_os::os_origin(&s.address));
remote_ui::find_view_url(output, os_origin.as_deref())
}
fn remember_remote_view(&mut self, spec: remote_ui::ViewSpec) {
let is_new = is_new_remote_view(self.last_view.as_ref(), &spec);
self.last_view = Some(spec.clone());
self.record_runtime_view_evidence();
if is_new {
self.push_line(&gutter(
ACCENT,
&remote_view_button("click or /view to open"),
));
}
}
pub(crate) fn skill_dirs(&self) -> Vec<std::path::PathBuf> {
let mut dirs = agent_skill_dirs(&self.cwd);
if let Some(d) = ensure_builtin_skills_dir() {
dirs.push(d);
}
if self.os_session.is_some() {
if let Some(cfg) = &self.os_config {
if let Some(d) = crate::a3s_os::ensure_capability_skill_dir(cfg) {
dirs.push(d);
}
}
}
dirs
}
fn refresh_after_auth(&mut self) {
if self.state == State::Idle {
if let Ok((s, _)) = self.rebuild_session(self.model.as_deref()) {
self.replace_session(s);
}
}
self.sync_runtime_tool();
let dirs = self.skill_dirs();
self.skill_count = count_skill_files(&dirs);
self.skills = load_skills(&dirs);
}
fn replace_session(&mut self, session: AgentSession) {
self.session = Arc::new(session);
self.session.register_dynamic_workflow_runtime();
self.sync_runtime_tool();
}
fn sync_runtime_tool(&self) {
match self.os_session.as_ref() {
Some(s) => self.session.register_dynamic_tool(std::sync::Arc::new(
crate::runtime_tool::RuntimeTool::new(s),
)),
None => self.session.unregister_dynamic_tool("runtime"),
}
}
fn open_config_in_ide(&mut self, path: &std::path::Path) {
let dir = path.parent().unwrap_or(std::path::Path::new("."));
let lines: Vec<String> = std::fs::read_to_string(path)
.unwrap_or_default()
.replace('\t', " ")
.lines()
.map(String::from)
.collect();
let mut ide = Ide::browse(ide_children(dir, 0), "config");
ide.file = Some(IdeFile::new(path.to_path_buf(), lines, false, false));
ide.focus_editor = true;
self.ide = Some(ide);
}
fn capture_workflow(&mut self, name: &str, args: Option<&serde_json::Value>) {
let Some((doc, label)) = workflow_doc_for_tool(name, args) else {
return;
};
self.last_workflow = Some(doc);
self.push_line(&Style::new().fg(ACCENT).render(&format!(" ⊞ {label}")));
}
fn open_readonly_in_ide(&mut self, title: &str, content: &str) {
let lines: Vec<String> = content.lines().map(String::from).collect();
let mut ide = Ide::browse(
ide_children(std::path::Path::new(&self.cwd), 0),
"workspace",
);
ide.file = Some(IdeFile::new(
std::path::PathBuf::from(title),
lines,
false,
true,
));
ide.focus_editor = true;
ide.flash = Some(ide_flash_line(ToastKind::Warning, "read-only"));
self.ide = Some(ide);
}
fn format_tool_log(&self) -> Option<String> {
format_tool_log_records(self.runtime.tool_log(), self.viewport_content_width())
}
fn history_recall(&mut self, up: bool) {
let pos = match (self.history_pos, up) {
(None, true) => self.history.len().saturating_sub(1),
(None, false) => return,
(Some(i), true) => i.saturating_sub(1),
(Some(i), false) => i + 1,
};
if pos >= self.history.len() {
self.history_pos = None;
self.textarea.clear();
} else {
self.history_pos = Some(pos);
self.textarea.set_value(&self.history[pos]);
}
}
fn update_viewport_with_stream(&mut self) {
if let Some(t) = self.last_paint {
if t.elapsed() < Duration::from_millis(33) {
return;
}
}
self.last_paint = Some(Instant::now());
let mut blocks: Vec<String> = self.messages.clone();
let body = thinking_block(&self.thinking, self.viewport_content_width());
if !body.is_empty() {
blocks.push(body);
}
let rendered = self.streaming.view();
if !rendered.is_empty() {
blocks.push(gutter(TN_GREEN, &rendered));
}
if let Some(tool) = self.runtime.live_tool() {
let on = self.blink_tick % 8 < 4; blocks.push(render_live_tool_activity(
&tool.name,
tool.args().as_ref(),
tool.output(),
self.viewport_content_width(),
on,
));
}
self.viewport
.set_content(&format!("\n{}\n", blocks.join("\n\n")));
}
fn rebuild_viewport(&mut self) {
self.selection = None; let full = self.messages.join("\n\n");
self.viewport.set_content(&format!("\n{full}\n")); }
pub(crate) fn input_height(&self) -> u16 {
self.textarea.height()
}
fn handle_approval_key(&mut self, key: &KeyEvent) -> Option<Cmd<Msg>> {
match key.code {
KeyCode::Up => {
self.approval_sel = self.approval_sel.saturating_sub(1);
None
}
KeyCode::Down => {
self.approval_sel = (self.approval_sel + 1).min(2);
None
}
KeyCode::Enter => Some(cmd::msg(self.apply_approval(self.approval_sel))),
KeyCode::Char('y' | 'Y') => Some(cmd::msg(self.apply_approval(0))),
KeyCode::Char('a' | 'A') => Some(cmd::msg(self.apply_approval(1))),
KeyCode::Char('n' | 'N') | KeyCode::Esc => Some(cmd::msg(self.apply_approval(2))),
KeyCode::Char(c @ '1'..='3') => {
Some(cmd::msg(self.apply_approval(c as usize - '1' as usize)))
}
_ => None,
}
}
fn handle_approval_mouse(&mut self, mouse: &MouseEvent) -> Option<Cmd<Msg>> {
if self.state != State::Awaiting {
return None;
}
let Some((_, label)) = &self.pending_tool else {
return None;
};
let width = (self.width as usize).min(u16::MAX as usize);
if width == 0 {
return None;
}
let mut prompt = approval_prompt(label, self.approval_sel);
let row_count = prompt.lines(width as u16, APPROVAL_PANEL_HEIGHT).len();
if row_count == 0 {
return None;
}
let y_offset = approval_overlay_y_offset(self.height as usize, row_count);
let row = mouse.row as usize;
let start = y_offset as usize;
if row < start || row >= start.saturating_add(row_count) {
return None;
}
prompt.set_y_offset(y_offset);
let before = prompt.selected_index();
match prompt.handle_mouse(mouse) {
Some(ChoicePromptMsg::Selected(index)) => Some(cmd::msg(self.apply_approval(index))),
Some(ChoicePromptMsg::Cancelled) => Some(cmd::msg(self.apply_approval(2))),
None => {
let after = prompt.selected_index().min(2);
if after != before {
self.approval_sel = after;
}
None
}
}
}
fn apply_approval(&mut self, choice: usize) -> Msg {
match choice {
0 => Msg::ModalConfirm(0), 1 => {
self.mode = Mode::Auto; Msg::ModalConfirm(0)
}
_ => Msg::ModalConfirm(1), }
}
fn overlay_approval(&self, composed: String) -> String {
if self.state != State::Awaiting {
return composed;
}
let Some((_, label)) = &self.pending_tool else {
return composed;
};
let menu = approval_menu_lines(label, self.approval_sel, self.width as usize);
self.overlay_list(composed, &menu)
}
}
fn approval_menu_lines(label: &str, selected: usize, width: usize) -> Vec<String> {
approval_prompt(label, selected).lines(width as u16, APPROVAL_PANEL_HEIGHT)
}
const APPROVAL_OVERLAY_ROWS_BELOW: usize = 5;
const APPROVAL_PANEL_HEIGHT: usize = 5;
fn approval_prompt(label: &str, selected: usize) -> ChoicePrompt {
ChoicePrompt::approval(format!("⏵ Allow {label}?"))
.selected(selected)
.indent(2)
.marker("❯")
.title_color(TN_YELLOW)
.text_color(TN_FG)
.muted_color(TN_GRAY)
.danger_color(TN_RED)
.selected_colors(Color::BrightWhite, ACCENT)
.hint("Enter select · ↑/↓ · 1–3 · Esc")
}
fn approval_overlay_y_offset(screen_height: usize, row_count: usize) -> u16 {
screen_height
.saturating_sub(APPROVAL_OVERLAY_ROWS_BELOW)
.saturating_sub(row_count)
.min(u16::MAX as usize) as u16
}
async fn run_smoke(session: Arc<AgentSession>) -> anyhow::Result<()> {
let prompt = std::env::var("A3S_CODE_TUI_PROMPT")
.unwrap_or_else(|_| "Reply with exactly one short sentence: what is 2 + 2?".to_string());
eprintln!("[smoke] prompt: {prompt}");
let (mut rx, join) = session.stream(prompt.as_str(), None).await?;
while let Some(event) = rx.recv().await {
match event {
AgentEvent::TextDelta { text } => print!("{text}"),
AgentEvent::ToolStart { name, .. } => eprintln!("\n[tool start] {name}"),
AgentEvent::ToolEnd {
name,
exit_code,
output,
..
} => eprintln!(
"[tool end] {name} (exit {exit_code}): {}",
output.lines().take(2).collect::<Vec<_>>().join(" | ")
),
AgentEvent::ConfirmationRequired {
tool_id, tool_name, ..
} => {
eprintln!("[confirm] auto-allowing {tool_name}");
let _ = session.confirm_tool_use(&tool_id, true, None).await;
}
AgentEvent::End { .. } => eprintln!("\n[end]"),
AgentEvent::Error { message } => eprintln!("\n[error] {message}"),
_ => {}
}
}
let _ = join.await;
Ok(())
}
pub async fn run(args: Vec<String>) -> anyhow::Result<()> {
let resuming = args.first().map(String::as_str) == Some("resume");
let explicit_id = if resuming { args.get(1).cloned() } else { None };
let mut session_id = explicit_id.clone().unwrap_or_else(new_session_id);
let (config_path, created_config) = match find_config() {
Some(p) => (p, false),
None => {
let p = default_config_path()
.ok_or_else(|| anyhow::anyhow!("no HOME directory found for ~/.a3s/config.acl"))?;
write_template_config(&p)
.map_err(|e| anyhow::anyhow!("failed to write starter config {p:?}: {e}"))?;
(p.to_string_lossy().into_owned(), true)
}
};
let agent = Arc::new(
Agent::new(config_path.clone())
.await
.map_err(|e| anyhow::anyhow!("failed to load agent from {config_path}: {e}"))?,
);
let workspace = std::env::current_dir()?.to_string_lossy().to_string();
let mut models: Vec<String> = Vec::new();
let mut model_ctx: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
let mut default_model: Option<String> = None;
let mut os_config: Option<OsConfig> = None;
if let Ok(cfg) =
a3s_code_core::config::CodeConfig::from_file(std::path::Path::new(&config_path))
{
for (p, m) in cfg.list_models() {
let id = format!("{}/{}", p.name, m.id);
model_ctx.insert(id.clone(), m.limit.context);
models.push(id);
}
default_model = cfg.default_model.clone();
os_config = cfg.os.clone();
}
let context_limit = default_model
.as_ref()
.map(|m| ctx_limit_for_model(&model_ctx, m))
.unwrap_or_else(|| resolve_ctx_limit(None));
let store_dir = std::path::Path::new(&workspace).join(".a3s/tui-sessions");
if resuming {
let mut saved: Vec<(String, std::time::SystemTime)> = std::fs::read_dir(&store_dir)
.into_iter()
.flatten()
.flatten()
.filter_map(|e| {
let p = e.path();
if p.extension().and_then(|x| x.to_str()) != Some("json") {
return None;
}
let id = p.file_stem()?.to_str()?.to_string();
let mtime = e.metadata().ok()?.modified().ok()?;
Some((id, mtime))
})
.collect();
saved.sort_by_key(|e| std::cmp::Reverse(e.1)); match &explicit_id {
Some(id) if !saved.iter().any(|(s, _)| s == id) => {
eprintln!("a3s: session '{id}' not found in {}", store_dir.display());
if saved.is_empty() {
eprintln!(" (no saved sessions in this directory)");
} else {
eprintln!(" available sessions (newest first):");
for (s, _) in saved.iter().take(10) {
eprintln!(" a3s code resume {s}");
}
}
return Ok(());
}
None => match saved.first() {
Some((s, _)) => session_id = s.clone(),
None => {
eprintln!(
"a3s: no saved sessions to resume in {}",
store_dir.display()
);
return Ok(());
}
},
_ => {}
}
}
let store: Arc<dyn a3s_code_core::store::SessionStore> = Arc::new(
a3s_code_core::store::FileSessionStore::new(&store_dir)
.await
.map_err(|e| anyhow::anyhow!("failed to open session store {store_dir:?}: {e}"))?,
);
let confirmation = a3s_code_core::hitl::ConfirmationPolicy::enabled()
.with_timeout(HITL_CONFIRM_TIMEOUT_MS, TimeoutAction::Reject);
let mut claude_dirs = agent_skill_dirs(&workspace);
let os_session = os_config.as_ref().and_then(crate::a3s_os::current_session);
if let Some(s) = &os_session {
crate::a3s_os::export_os_env(s);
if let Some(dir) = os_config
.as_ref()
.and_then(crate::a3s_os::ensure_capability_skill_dir)
{
claude_dirs.push(dir);
}
}
let instructions = project_instructions(&workspace);
let os_address = os_session.as_ref().map(|s| s.address.clone());
let ctx_ready = panels::ctx::ctx_available();
let with_instr = |o: SessionOptions| {
let mut parts: Vec<String> = Vec::new();
if let Some(i) = &instructions {
parts.push(i.clone());
}
if let Some(addr) = &os_address {
parts.push(os_platform_guide(addr));
}
if ctx_ready {
parts.push(panels::ctx::ctx_history_guide());
}
if parts.is_empty() {
o
} else {
o.with_prompt_slots(SystemPromptSlots::default().with_extra(parts.join("\n\n")))
}
};
let manifest_backend = ManifestWorkspaceBackend::new(std::path::PathBuf::from(&workspace));
let workspace_manifest = manifest_backend.manifest();
let initial_manifest = workspace_manifest.snapshot();
let initial_files = initial_manifest.file_paths();
let workspace_manifest_rx = Arc::new(Mutex::new(workspace_manifest.subscribe()));
let workspace_services = WorkspaceServices::local_with_manifest_backend(manifest_backend);
let session = match agent.resume_session(
session_id.as_str(),
with_instr(with_recent_workspace_context(
tui_session_options(confirmation.clone())
.with_session_store(store.clone())
.with_workspace_backend(workspace_services.clone())
.with_skill_dirs(claude_dirs.clone())
.with_auto_save(true)
.with_auto_compact(true)
.with_auto_compact_threshold(auto_compact_threshold_for(context_limit))
.with_file_memory(memory_dir())
.with_max_parallel_tasks(8)
.with_auto_delegation_enabled(true)
.with_auto_parallel_delegation(true)
.with_manual_delegation_enabled(true),
&workspace_manifest,
)),
) {
Ok(s) => s,
Err(_) => agent.session(
workspace.clone(),
Some(with_instr(with_recent_workspace_context(
tui_session_options(confirmation.clone())
.with_session_store(store.clone())
.with_session_id(session_id.as_str())
.with_workspace_backend(workspace_services.clone())
.with_skill_dirs(claude_dirs.clone())
.with_auto_save(true)
.with_auto_compact(true)
.with_auto_compact_threshold(auto_compact_threshold_for(context_limit))
.with_file_memory(memory_dir())
.with_max_parallel_tasks(8)
.with_auto_delegation_enabled(true)
.with_auto_parallel_delegation(true)
.with_manual_delegation_enabled(true),
&workspace_manifest,
))),
)?,
};
session.register_dynamic_workflow_runtime();
if let Some(os) = os_session.as_ref() {
session.register_dynamic_tool(std::sync::Arc::new(crate::runtime_tool::RuntimeTool::new(
os,
)));
}
let (width, height) = a3s_tui::terminal::Terminal::size().unwrap_or((80, 24));
let resumed = session.history();
let mut initial_messages: Vec<String> = resumed
.iter()
.filter_map(|m| {
let text = m.text();
if text.trim().is_empty() {
return None;
}
match m.role.as_str() {
"user" => Some(gutter(ACCENT, text.trim())),
"assistant" => {
let mut md = StreamingMarkdown::new(transcript_markdown_width_for(width));
md.push(&text);
Some(gutter(TN_GREEN, &md.view()))
}
_ => None,
}
})
.collect();
let history_seed: Vec<String> = resumed
.iter()
.filter(|m| m.role == "user")
.map(|m| m.text().trim().to_string())
.filter(|t| !t.is_empty())
.collect();
if let Some(s) = &os_session {
if !initial_messages.is_empty() {
initial_messages.insert(
0,
Style::new().fg(TN_GRAY).render(&format!(
" ✓ signed in to OS as {} · capabilities skill active · /logout to sign out",
s.display_label()
)),
);
}
}
let session = Arc::new(session);
if std::env::var_os("A3S_CODE_TUI_SMOKE").is_some() {
return run_smoke(session).await;
}
let keymap = Keymap::new()
.bind(
KeyBinding::new(KeyCode::PageUp),
Action::ScrollUp,
"Scroll up",
)
.bind(
KeyBinding::new(KeyCode::PageDown),
Action::ScrollDown,
"Scroll down",
)
.bind(
KeyBinding::ctrl(KeyCode::Home),
Action::ScrollTop,
"Scroll to top",
)
.bind(
KeyBinding::ctrl(KeyCode::End),
Action::ScrollBottom,
"Scroll to bottom",
);
remote_ui::prime_webview_lookup();
let mut app = App {
session,
agent: agent.clone(),
store: store.clone(),
confirmation,
session_id: session_id.clone(),
models,
model_ctx,
context_limit,
last_prompt_tokens: 0,
ctx_warned_tier: 0,
model_menu: None,
model_tab: 0,
llm_override: None,
os_config,
os_session,
os_refreshing: false,
os_gateway_models: None,
os_gateway_error: None,
last_view: None,
runtime_expectation: None,
effort: 2, effort_panel: None,
theme_panel: None,
quit_armed: None,
last_activity: Instant::now(),
auto_reviewed: false,
shell_mode: false,
research_mode: false,
review_pending: false,
sleep_pending: false,
review: None,
review_open: false,
flow: None,
pending_flow_subcommand: None,
agent_picker: None,
pending_agent_subcommand: None,
agent_dev: None,
mcp_picker: None,
pending_mcp_subcommand: None,
mcp_dev: None,
skill_picker: None,
pending_skill_subcommand: None,
skill_dev: None,
okf_picker: None,
pending_okf_subcommand: None,
okf_dev: None,
autonomy_restore: None,
ctx_ready,
ctx_hits: Vec::new(),
pending_ctx: None,
loop_continuation: false,
turn_text: String::new(),
selection: None,
last_workflow: None,
pending_images: Vec::new(),
goal: None,
goal_since: None,
loop_remaining: 0,
runtime: RuntimeProjection::default(),
turn_had_agent_activity: false,
turn_text_after_activity: false,
ultracode_synthesis_inflight: false,
ultracode_synthesis_used: false,
instructions,
workspace_manifest,
workspace_manifest_rx,
workspace_services,
gradient_until: None,
gradient_frame: 0,
effort_anim: None,
compact_summary: None,
btw: None,
viewport: Viewport::new(width.saturating_sub(1), height.saturating_sub(7)),
textarea: Textarea::new()
.with_height(1)
.with_auto_grow(8) .with_width(width.saturating_sub((PAD + 2) as u16)) .with_submit_on_enter(true),
spinner: Spinner::new().with_title(""),
streaming: StreamingMarkdown::new(transcript_markdown_width_for(width)),
got_delta: false,
compacting: None,
updating: None,
last_paint: None,
thinking: String::new(),
state: State::Idle,
messages: initial_messages,
rx: None,
stream_join: None,
host_progress_inflight: false,
interrupting: false,
pending_tool: None,
approval_sel: 0,
history: history_seed,
history_pos: None,
model: default_model,
output_tokens: 0,
stream_started: None,
blink_tick: 0,
anim: 0,
mode: Mode::Default,
queue: BinaryHeap::new(),
seq: 0,
running_task: None,
plan: Vec::new(),
top: None,
top_history: TopProcessHistory::default(),
top_scroll: 0,
top_sel: 0,
top_focus: None,
ide: None,
memory: None,
asset_list: None,
runtime_activity: None,
kb: None,
loop_panel: None,
help_open: false,
help_scroll: 0,
completed: 0,
branch: git_branch(&workspace),
slash_sel: 0,
files: initial_files,
at_expanded: std::collections::HashSet::new(),
file_sel: 0,
skill_count: count_skill_files(&claude_dirs),
skills: load_skills(&claude_dirs),
disabled_skills: load_disabled_skills(),
plugins_panel: None,
update_available: None,
cwd: workspace.clone(),
width,
height,
keymap,
};
if created_config {
app.messages.push(gutter(
ACCENT,
"Welcome to a3s code! Generated a starter ~/.a3s/config.acl — fill in your \
provider apiKey/baseUrl + model, Ctrl+S to save, Esc to close, then restart \
`a3s code` to load it.",
));
app.open_config_in_ide(std::path::Path::new(&config_path));
app.rebuild_viewport();
}
let launch_model = app.model.clone();
if let Ok((s, _)) = app.rebuild_session(launch_model.as_deref()) {
app.replace_session(s);
}
ProgramBuilder::new(app)
.with_alt_screen()
.with_mouse_support()
.with_fps(30)
.run()
.await?;
if UPGRADE_ON_EXIT.load(std::sync::atomic::Ordering::Relaxed) {
let latest = LATEST
.lock()
.ok()
.and_then(|g| g.clone())
.unwrap_or_default();
match crate::update::perform_upgrade(&latest) {
Ok(bin) => {
let restart_args = ["code", "resume", session_id.as_str()];
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let err = std::process::Command::new(&bin).args(restart_args).exec();
eprintln!(
"\n⚠ updated, but restart via {} failed: {err}",
bin.display()
);
if let Ok(exe) = std::env::current_exe() {
let err = std::process::Command::new(&exe).args(restart_args).exec();
eprintln!("⚠ fallback restart via {} failed: {err}", exe.display());
}
eprintln!(
"✓ updated to a3s {latest}; resume manually with: a3s code resume {session_id}\n"
);
}
#[cfg(not(unix))]
{
match std::process::Command::new(&bin).args(restart_args).status() {
Ok(status) if status.success() => {}
Ok(status) => eprintln!(
"\n⚠ updated, but restart exited with status {status}; resume manually with: a3s code resume {session_id}\n"
),
Err(err) => eprintln!(
"\n⚠ updated, but restart failed: {err}; resume manually with: a3s code resume {session_id}\n"
),
}
}
}
Err(error) => {
eprintln!("\n✗ upgrade failed: {error}");
eprintln!("get the latest from https://github.com/A3S-Lab/Cli/releases/latest\n");
}
}
return Ok(());
}
println!("\n session saved · resume it with: a3s code resume {session_id}\n");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn rgb(color: Color) -> (u8, u8, u8) {
match color {
Color::Rgb(r, g, b) => (r, g, b),
other => panic!("expected RGB color, got {other:?}"),
}
}
fn contains_cjk(s: &str) -> bool {
s.chars().any(|ch| {
('\u{3400}'..='\u{4dbf}').contains(&ch)
|| ('\u{4e00}'..='\u{9fff}').contains(&ch)
|| ('\u{f900}'..='\u{faff}').contains(&ch)
})
}
#[test]
fn tui_palette_tracks_design_tokens() {
assert_eq!(rgb(ACCENT), (0, 112, 243));
assert_eq!(TN_GREEN, ACCENT);
assert_eq!(rgb(TN_YELLOW), (245, 166, 35));
assert_eq!(rgb(TN_RED), (238, 0, 0));
assert_eq!(rgb(TN_CYAN), (80, 227, 194));
assert_eq!(rgb(TN_FG), (237, 237, 237));
assert_eq!(rgb(TN_GRAY), (143, 143, 143));
assert_eq!(
BRAND_GRADIENT,
[
Color::Rgb(0, 124, 240),
Color::Rgb(0, 223, 216),
TN_PURPLE,
Color::Rgb(255, 0, 128),
Color::Rgb(255, 77, 77),
Color::Rgb(249, 203, 40),
]
);
}
#[test]
fn agent_chrome_theme_maps_tui_roles_to_code_palette() {
let theme = agent_chrome_theme();
assert_eq!(theme.primary, ACCENT);
assert_eq!(theme.fg, TN_FG);
assert_eq!(theme.muted, TN_GRAY);
assert_eq!(theme.border, TN_GRAY);
assert_eq!(theme.success, TN_GREEN);
assert_eq!(theme.warning, TN_ORANGE);
assert_eq!(theme.error, TN_RED);
assert_eq!(theme.surface, SURFACE_SOFT);
assert_eq!(theme.highlight, SURFACE_SELECTED);
let chrome = agent_chrome(&theme);
let rendered = chrome.tool_status("Running").view(24);
assert!(
rendered.contains(&ACCENT.fg_ansi()),
"agent chrome should render code primary color: {rendered:?}"
);
}
#[test]
fn remote_view_button_is_styled_but_clickable_by_marker() {
let rendered = remote_view_button("click or /view to open");
let plain = a3s_tui::style::strip_ansi(&rendered);
assert!(plain.contains(VIEW_BUTTON_MARKER), "{plain}");
assert!(plain.contains("click or /view to open"), "{plain}");
assert!(
rendered.contains("\x1b["),
"button should carry ANSI styling"
);
}
#[test]
fn remote_view_click_tolerates_small_terminal_mouse_drift() {
let rendered = remote_view_button("click or /view to open");
let view = format!("plain transcript\n{rendered}\nnext line");
assert!(is_remote_view_click(
&view,
Selection {
anchor: (1, 4),
head: (1, 6),
}
));
assert!(!is_remote_view_click(
&view,
Selection {
anchor: (1, 4),
head: (1, 12),
}
));
assert!(!is_remote_view_click(
&view,
Selection {
anchor: (1, 4),
head: (2, 4),
}
));
assert!(!is_remote_view_click(
&view,
Selection {
anchor: (0, 4),
head: (0, 4),
}
));
}
#[test]
fn remote_view_click_marker_is_case_insensitive_after_ansi_strip() {
let view = format!(
" {}\n",
Style::new()
.fg(Color::BrightWhite)
.bg(ACCENT)
.render(" ↗ Open View ")
);
assert!(is_remote_view_click(
&view,
Selection {
anchor: (0, 3),
head: (0, 3),
}
));
}
#[test]
fn quit_key_accepts_control_c_terminal_variants() {
let key = |code, modifiers| KeyEvent { code, modifiers };
assert!(is_quit_key(&key(KeyCode::Char('c'), KeyModifiers::CONTROL)));
assert!(is_quit_key(&key(
KeyCode::Char('C'),
KeyModifiers::CONTROL | KeyModifiers::SHIFT
)));
assert!(!is_quit_key(&key(KeyCode::Char('c'), KeyModifiers::NONE)));
assert!(!is_quit_key(&key(
KeyCode::Char('v'),
KeyModifiers::CONTROL
)));
}
#[test]
fn quit_confirmation_requires_second_press_inside_window() {
let now = Instant::now();
assert!(!quit_is_confirmed(None, now));
if let Some(recent) = now.checked_sub(Duration::from_millis(500)) {
assert!(quit_is_confirmed(Some(recent), now));
}
if let Some(stale) = now.checked_sub(Duration::from_secs(3)) {
assert!(!quit_is_confirmed(Some(stale), now));
}
}
#[test]
fn footer_uses_shared_status_components() {
let status = render_session_status_line(
"/Users/roylin/code/a3s",
Some("main"),
Some("openai/gpt-5"),
128_000,
90_000,
0,
[SessionStatusChip::new("⚙", "1 running").color(TN_GRAY)],
72,
);
let status_plain = a3s_tui::style::strip_ansi(&status);
assert_eq!(a3s_tui::style::visible_len(&status), 72);
assert!(status_plain.contains("a3s git:(main)"), "{status_plain}");
assert!(
status_plain.contains("gpt-5 (128k context)"),
"{status_plain}"
);
assert!(status_plain.contains("ctx:70%"), "{status_plain}");
assert!(status_plain.contains("⚙ 1 running"), "{status_plain}");
assert!(status.contains("\x1b["), "status should be styled");
let mode = render_mode_status_line(Mode::Auto, 48);
let mode_plain = a3s_tui::style::strip_ansi(&mode);
assert_eq!(a3s_tui::style::visible_len(&mode), 48);
assert!(mode_plain.starts_with(" ⏵⏵ auto mode on"), "{mode_plain}");
assert!(mode_plain.contains("/help"), "{mode_plain}");
assert!(mode.contains("\x1b["), "mode line should be styled");
}
#[test]
fn jump_to_latest_hint_uses_shared_inline_action() {
let hint = jump_to_latest_hint(48);
let plain = a3s_tui::style::strip_ansi(&hint);
assert_eq!(a3s_tui::style::visible_len(&hint), 48);
assert!(plain.contains("↓ more below"), "{plain}");
assert!(plain.contains("Shift+End"), "{plain}");
assert!(hint.contains("\x1b["), "hint should be styled");
assert_eq!(jump_to_latest_hint(0), "");
}
#[test]
fn tui_session_options_sets_separate_tool_timeout() {
let confirmation = a3s_code_core::hitl::ConfirmationPolicy::enabled()
.with_timeout(HITL_CONFIRM_TIMEOUT_MS, TimeoutAction::Reject);
let opts = tui_session_options(confirmation);
let dbg = format!("{opts:?}");
assert_ne!(HITL_CONFIRM_TIMEOUT_MS, TOOL_EXEC_TIMEOUT_MS);
assert!(
dbg.contains(&format!("tool_timeout_ms: Some({TOOL_EXEC_TIMEOUT_MS})")),
"{dbg}"
);
}
#[test]
fn approval_menu_uses_shared_choice_prompt() {
let lines = approval_menu_lines(
"Bash(cargo test very-long-filter-name-that-should-not-overflow)",
1,
42,
);
let plain = lines
.iter()
.map(|line| a3s_tui::style::strip_ansi(line))
.collect::<Vec<_>>();
assert_eq!(plain.len(), 5);
assert!(plain[0].contains("Allow"), "{plain:?}");
assert!(plain[1].contains("1. Yes"), "{plain:?}");
assert!(plain[2].contains("2. Yes, and"), "{plain:?}");
assert!(plain[3].contains("3. No"), "{plain:?}");
assert!(plain[4].contains("Enter select"), "{plain:?}");
assert!(
lines
.iter()
.all(|line| a3s_tui::style::visible_len(line) <= 42),
"{plain:?}"
);
assert!(lines[2].contains("\x1b["), "selected row is styled");
}
#[test]
fn approval_prompt_mouse_wheel_moves_selection_at_overlay_offset() {
use a3s_tui::event::{MouseEvent, MouseEventKind};
let width = 42;
let lines = approval_menu_lines("Bash(cargo test)", 0, width);
let y_offset = approval_overlay_y_offset(18, lines.len());
let mut prompt = approval_prompt("Bash(cargo test)", 0);
prompt.set_y_offset(y_offset);
let msg = prompt.handle_mouse(&MouseEvent {
kind: MouseEventKind::ScrollDown,
column: 0,
row: y_offset + 1,
modifiers: KeyModifiers::NONE,
});
assert_eq!(msg, None);
assert_eq!(prompt.selected_index(), 1);
}
#[test]
fn approval_prompt_click_selects_choice_at_overlay_offset() {
use a3s_tui::event::{MouseButton, MouseEvent, MouseEventKind};
let width = 42;
let lines = approval_menu_lines("Bash(cargo test)", 0, width);
let y_offset = approval_overlay_y_offset(18, lines.len());
let mut prompt = approval_prompt("Bash(cargo test)", 0);
prompt.set_y_offset(y_offset);
let msg = prompt.handle_mouse(&MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 2,
row: y_offset + 2,
modifiers: KeyModifiers::NONE,
});
assert_eq!(msg, Some(ChoicePromptMsg::Selected(1)));
}
#[test]
fn effort_ladder_is_monotonic_and_well_formed() {
assert_eq!(ULTRACODE, EFFORT_LEVELS.len() - 1);
assert_eq!(EFFORT_LEVELS[ULTRACODE].label, "ultracode");
for w in EFFORT_LEVELS[..=ULTRACODE].windows(2) {
assert!(
w[1].thinking_budget >= w[0].thinking_budget,
"thinking budget regressed"
);
assert!(
w[1].max_tool_rounds >= w[0].max_tool_rounds,
"tool-round budget regressed"
);
assert!(
w[1].max_continuation_turns >= w[0].max_continuation_turns,
"continuation budget regressed"
);
}
assert!(
EFFORT_LEVELS[1].guideline.is_none(),
"medium should be the baseline"
);
for (i, p) in EFFORT_LEVELS.iter().enumerate() {
if i != 1 {
assert!(
p.guideline.is_some(),
"level {} has no depth steer",
p.label
);
}
}
}
use a3s_code_core::llm::{
ContentBlock, LlmClient, LlmResponse, Message, StreamEvent, TokenUsage, ToolDefinition,
};
use async_trait::async_trait;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
#[derive(Clone, Default)]
struct CapturedLlmTurn {
system: Option<String>,
tools: Vec<String>,
}
struct CaptureLlmClient {
turns: Mutex<Vec<CapturedLlmTurn>>,
responses: Mutex<VecDeque<LlmResponse>>,
}
#[async_trait]
impl LlmClient for CaptureLlmClient {
async fn complete(
&self,
_messages: &[Message],
system: Option<&str>,
tools: &[ToolDefinition],
) -> anyhow::Result<LlmResponse> {
self.record(system, tools);
Ok(self.next_response())
}
async fn complete_streaming(
&self,
_messages: &[Message],
system: Option<&str>,
tools: &[ToolDefinition],
_cancel_token: CancellationToken,
) -> anyhow::Result<mpsc::Receiver<StreamEvent>> {
self.record(system, tools);
let response = self.next_response();
let (tx, rx) = mpsc::channel(2);
tokio::spawn(async move {
let _ = tx.send(StreamEvent::Done(response)).await;
});
Ok(rx)
}
}
impl CaptureLlmClient {
fn new(responses: Vec<LlmResponse>) -> Self {
Self {
turns: Mutex::new(Vec::new()),
responses: Mutex::new(responses.into()),
}
}
fn record(&self, system: Option<&str>, tools: &[ToolDefinition]) {
self.turns.lock().unwrap().push(CapturedLlmTurn {
system: system.map(str::to_string),
tools: tools.iter().map(|tool| tool.name.clone()).collect(),
});
}
fn next_response(&self) -> LlmResponse {
self.responses
.lock()
.unwrap()
.pop_front()
.unwrap_or_else(done_response)
}
fn turns(&self) -> Vec<CapturedLlmTurn> {
self.turns.lock().unwrap().clone()
}
}
fn tool_call_response(name: &str, input: serde_json::Value) -> LlmResponse {
LlmResponse {
message: Message {
role: "assistant".into(),
content: vec![ContentBlock::ToolUse {
id: "toolu_test".into(),
name: name.into(),
input,
}],
reasoning_content: None,
},
usage: TokenUsage::default(),
stop_reason: Some("tool_use".into()),
token_logprobs: Vec::new(),
meta: None,
}
}
fn done_response() -> LlmResponse {
LlmResponse {
message: Message {
role: "assistant".into(),
content: vec![ContentBlock::Text {
text: "DONE".into(),
}],
reasoning_content: None,
},
usage: TokenUsage::default(),
stop_reason: Some("stop".into()),
token_logprobs: Vec::new(),
meta: None,
}
}
fn test_config(path: &std::path::Path) {
std::fs::write(
path,
"default_model = \"openai/x\"\n\
providers \"openai\" {\n apiKey = \"x\"\n baseUrl = \"http://127.0.0.1:1\"\n \
models \"x\" { name = \"x\" }\n}\n",
)
.unwrap();
}
#[tokio::test]
async fn parallel_opts_register_parallel_task() {
let dir = std::env::temp_dir().join(format!("a3s-ptask-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let cfg = dir.join("config.acl");
test_config(&cfg);
let agent = a3s_code_core::Agent::new(cfg.to_string_lossy().to_string())
.await
.unwrap();
let opts = SessionOptions::new()
.with_max_parallel_tasks(8)
.with_auto_delegation_enabled(true)
.with_auto_parallel_delegation(true)
.with_manual_delegation_enabled(true)
.with_planning_mode(a3s_code_core::PlanningMode::Enabled)
.with_goal_tracking(true)
.with_max_tool_rounds(200);
let session = agent
.session(dir.to_string_lossy().to_string(), Some(opts))
.unwrap();
session.register_dynamic_workflow_runtime();
let names = session.tool_names();
let _ = std::fs::remove_dir_all(&dir);
assert!(
names.contains(&"dynamic_workflow".to_string()),
"dynamic_workflow registered; got {names:?}"
);
assert!(
names.contains(&"parallel_task".to_string()),
"parallel_task registered; got {names:?}"
);
assert!(
names.contains(&"task".to_string()),
"task registered; got {names:?}"
);
}
#[test]
fn format_tool_log_empty_is_none() {
assert!(format_tool_log_records(&[], 80).is_none());
}
#[test]
fn format_tool_log_renders_bounded_headers_args_and_output() {
let recs = vec![
ToolCallRecord {
name: "read".into(),
args: Some(serde_json::json!({"file_path": "/x"})),
output: "hello\n".into(),
exit_code: 0,
},
ToolCallRecord {
name: "bash".into(),
args: None,
output: String::new(),
exit_code: 2,
},
];
let out = format_tool_log_records(&recs, 48).unwrap();
let plain = a3s_tui::style::strip_ansi(&out);
assert!(plain.contains("#1 · Read /x · ok"), "{plain}");
assert!(plain.contains("args: {\"file_path\":\"/x\"}"), "{plain}");
assert!(
plain.contains(" hello"),
"output should be indented: {plain}"
);
assert!(plain.contains("#2 · Ran · exit 2"), "{plain}");
for line in out.lines() {
assert!(
a3s_tui::style::visible_len(line) <= 48,
"log line should be bounded: {line:?}"
);
}
}
#[test]
fn format_tool_log_tails_long_output() {
let recs = vec![ToolCallRecord {
name: "bash".into(),
args: Some(serde_json::json!({
"command": "cargo test a-very-long-filter-that-must-not-overflow -- --nocapture"
})),
output: (0..30)
.map(|i| format!("line-{i}-with-a-long-payload-that-must-stay-inside-the-editor"))
.collect::<Vec<_>>()
.join("\n"),
exit_code: 0,
}];
let out = format_tool_log_records(&recs, 52).unwrap();
let plain = a3s_tui::style::strip_ansi(&out);
assert!(plain.contains("... +6 earlier lines"), "{plain}");
assert!(!plain.contains("line-0-with"), "{plain}");
assert!(plain.contains("line-6-with"), "{plain}");
for line in out.lines() {
assert!(
a3s_tui::style::visible_len(line) <= 52,
"log line should be bounded: {line:?}"
);
}
}
#[test]
fn format_tool_log_respects_narrow_editor_width() {
let recs = vec![ToolCallRecord {
name: "bash".into(),
args: Some(serde_json::json!({
"command": "printf a-very-long-line-that-used-to-force-a-minimum-width"
})),
output: "a-very-long-output-line-that-must-be-clipped\n".into(),
exit_code: 0,
}];
let out = format_tool_log_records(&recs, 16).unwrap();
for line in out.lines() {
assert!(
a3s_tui::style::visible_len(line) <= 16,
"log line should respect narrow width: {line:?}"
);
}
}
#[test]
fn pending_tool_label_is_taken_only_for_matching_tool_id() {
let mut pending = Some(("tool-a".to_string(), "edit file".to_string()));
assert!(take_pending_tool_label(&mut pending, "tool-b").is_none());
assert_eq!(
pending,
Some(("tool-a".to_string(), "edit file".to_string()))
);
assert_eq!(
take_pending_tool_label(&mut pending, "tool-a"),
Some("edit file".to_string())
);
assert!(pending.is_none());
}
#[tokio::test]
async fn confirmation_resume_rearms_spinner_and_stream_pump() {
let cmd = resume_after_pending_confirmation_cmd(None);
match cmd.await {
a3s_tui::cmd::CmdResult::Batch(cmds) => {
assert_eq!(cmds.len(), 1, "spinner should resume without an rx");
}
_ => panic!("expected batched resume command"),
}
let (_tx, rx) = mpsc::channel::<AgentEvent>(1);
let cmd = resume_after_pending_confirmation_cmd(Some(std::sync::Arc::new(
tokio::sync::Mutex::new(rx),
)));
match cmd.await {
a3s_tui::cmd::CmdResult::Batch(cmds) => {
assert_eq!(cmds.len(), 2, "spinner and stream pump should resume");
}
_ => panic!("expected batched resume command"),
}
}
#[test]
fn deep_research_prompt_directs_research_and_keeps_query() {
let p = deep_research_prompt("rust async runtimes", false);
assert!(p.contains("rust async runtimes"), "{p}");
let lo = p.to_lowercase();
assert!(lo.contains("deep research"), "{p}");
assert!(p.contains("dynamic_workflow"), "{p}");
assert!(lo.contains("web search") && lo.contains("web_fetch"), "{p}");
assert!(lo.contains("source"), "should ask to cite sources: {p}");
assert!(
p.contains("step_name: \"parallel_task\"") || p.contains("parallel_task"),
"{p}"
);
assert!(p.contains(".a3s/research/<slug>/"), "{p}");
assert!(p.contains("standalone HTML"), "{p}");
}
#[test]
fn deep_research_prompt_uses_os_runtime_when_selected_without_requiring_remoteui() {
let p = deep_research_prompt("comprehensive rust async runtimes comparison", true);
assert!(p.contains("rust async runtimes"), "{p}");
assert!(p.contains("dynamic_workflow"), "{p}");
assert!(
p.contains("OS Runtime fan-out") && p.contains("runtime"),
"{p}"
);
assert!(!p.contains("Runtime evidence is required"), "{p}");
assert!(p.contains("allowed_tools: [\"runtime\"]"), "{p}");
assert!(p.contains("result.exitCode !== 0"), "{p}");
assert!(p.contains("runtime tool failed"), "{p}");
assert!(p.contains("naturally returns"), "{p}");
assert!(p.contains(".view") && p.contains("viewUrl"), "{p}");
assert!(
p.contains("Markdown report") && p.contains("HTML page"),
"{p}"
);
}
#[test]
fn deep_research_os_runtime_selection_is_adaptive() {
assert!(!should_use_os_runtime_for_deep_research(
"rust async runtimes",
true
));
assert!(should_use_os_runtime_for_deep_research(
"全面调研 2026 年多智能体运行时市场、最新论文、竞品和趋势",
true
));
assert!(!should_use_os_runtime_for_deep_research(
"全面调研 2026 年多智能体运行时市场",
false
));
assert!(!should_use_os_runtime_for_deep_research(
"本地分析一下这个 README",
true
));
}
#[test]
fn deep_research_workflow_args_use_runtime_then_local_fallback() {
let args = deep_research_workflow_args("rust async runtimes", true);
let source = args["source"].as_str().unwrap();
assert_eq!(args["input"]["query"], "rust async runtimes");
assert_eq!(args["input"]["os_runtime"], true);
assert_eq!(args["allowed_tools"], serde_json::json!(["runtime"]));
assert!(source.contains("ctx.tool(\"runtime\""), "{source}");
assert!(source.contains("runtime_error"), "{source}");
assert!(source.contains("local_fallback"), "{source}");
assert!(source.contains("step_name: \"parallel_task\""), "{source}");
assert!(source.contains("agent: \"general\""), "{source}");
assert!(!source.contains("agent: \"explore\""), "{source}");
}
#[test]
fn deep_research_synthesis_prompt_uses_host_workflow_evidence() {
let prompt = deep_research_synthesis_prompt(
"rust async runtimes",
true,
r#"{"mode":"os_runtime"}"#,
Some(&serde_json::json!({
"dynamic_workflow": {
"snapshot": {
"steps": {
"runtime_research": {
"output": { "name": "runtime" }
}
}
}
}
})),
);
assert!(
prompt.contains("Do not call `dynamic_workflow` again"),
"{prompt}"
);
assert!(prompt.contains("rust async runtimes"), "{prompt}");
assert!(prompt.contains("mode\":\"os_runtime"), "{prompt}");
assert!(prompt.contains("OS Runtime was selected"), "{prompt}");
assert!(prompt.contains("do not claim"), "{prompt}");
}
#[test]
fn deep_research_metadata_detects_runtime_and_local_parallel_evidence() {
let metadata = serde_json::json!({
"dynamic_workflow": {
"snapshot": {
"steps": {
"runtime_research": {
"output": { "name": "runtime" }
},
"local_fallback": {
"output": { "tool": "parallel_task" }
}
}
}
}
});
assert!(json_contains_tool_evidence(&metadata, "runtime"));
assert!(json_contains_tool_evidence(&metadata, "parallel_task"));
assert!(!json_contains_tool_evidence(&metadata, "program"));
}
#[test]
fn deep_research_goal_is_a_research_north_star_with_query() {
let g = deep_research_goal("rust async runtimes");
assert!(g.contains("rust async runtimes"), "{g}");
assert!(g.to_lowercase().contains("research"), "{g}");
}
#[test]
fn scrollbar_blank_when_content_fits() {
let out = append_scrollbar("a\nb\nc", 5, 3, 100);
assert_eq!(out.lines().count(), 3);
for line in out.lines() {
assert!(line.ends_with(' '), "no-overflow gutter blank: {line:?}");
assert!(!line.contains('█') && !line.contains('│'));
}
}
#[test]
fn scrollbar_thumb_tracks_position() {
let view = "r0\nr1\nr2\nr3"; let top = append_scrollbar(view, 4, 40, 0);
assert!(top.lines().next().unwrap().contains('█'), "thumb at top");
let bottom = append_scrollbar(view, 4, 40, 100);
assert!(
bottom.lines().last().unwrap().contains('█'),
"thumb at bottom"
);
assert!(top.lines().all(|l| l.contains('█') || l.contains('│')));
}
#[test]
fn osc52_wraps_base64_in_envelope() {
let s = osc52_copy("hi");
assert!(s.starts_with("\u{1b}]52;c;") && s.ends_with('\u{7}'));
assert!(s.contains("aGk=")); }
#[test]
fn slice_cols_handles_ascii_and_wide() {
assert_eq!(slice_cols("hello", 1, 4), "ell");
assert_eq!(slice_cols("hello", 0, 100), "hello");
assert_eq!(slice_cols("あい", 0, 2), "あ");
assert_eq!(slice_cols("あい", 2, 4), "い");
}
#[test]
fn selection_to_text_extracts_span_across_rows() {
let view = " hello world\n second line\n third";
let t = selection_to_text(view, 0, 2, 1, 8);
assert_eq!(t, "hello world\n second");
}
#[test]
fn mouse_selection_uses_release_position_clamped_to_viewport() {
assert_eq!(viewport_mouse_cell(2, 40, 4, 20), Some((2, 20)));
assert_eq!(viewport_mouse_cell(4, 2, 4, 20), None);
assert_eq!(viewport_mouse_cell_clamped(9, 40, 4, 20), Some((3, 20)));
assert_eq!(viewport_mouse_cell_clamped(0, 1, 0, 20), None);
}
#[test]
fn highlight_selection_touches_only_selected_rows() {
let view = "row zero\nrow one\nrow two";
let out = highlight_selection(view, 1, 0, 1, 7);
let lines: Vec<&str> = out.split('\n').collect();
assert_eq!(lines[0], "row zero"); assert_eq!(lines[2], "row two"); assert!(lines[1].contains("row one")); assert!(lines[1].contains('\u{1b}')); }
#[tokio::test]
async fn web_tools_registered_for_q_research_mode() {
let dir = std::env::temp_dir().join(format!(
"a3s-research-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let cfg = dir.join("config.acl");
test_config(&cfg);
let agent = a3s_code_core::Agent::new(cfg.to_string_lossy().to_string())
.await
.unwrap();
let session = agent
.session(dir.to_string_lossy().to_string(), None)
.unwrap();
let names = session.tool_names();
let _ = std::fs::remove_dir_all(&dir);
assert!(
names.contains(&"web_search".to_string()) && names.contains(&"web_fetch".to_string()),
"the `?` deep-research mode relies on web_search + web_fetch; got {names:?}"
);
}
#[test]
fn tui_default_policy_allows_readonly_research_tools() {
use a3s_code_core::permissions::PermissionDecision;
let policy = tui_permission_policy();
assert_eq!(
policy.check(
"web_fetch",
&serde_json::json!({"url": "https://example.com"})
),
PermissionDecision::Allow
);
assert_eq!(
policy.check("web_search", &serde_json::json!({"query": "a3s"})),
PermissionDecision::Allow
);
assert_eq!(
policy.check("read", &serde_json::json!({"file_path": "README.md"})),
PermissionDecision::Allow
);
assert_eq!(
policy.check(
"write",
&serde_json::json!({"file_path": "x", "content": "y"})
),
PermissionDecision::Ask,
"mutating tools must still go through TUI confirmation"
);
}
#[tokio::test]
async fn tui_session_policy_does_not_block_web_fetch() {
let dir = std::env::temp_dir().join(format!(
"a3s-web-fetch-policy-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let cfg = dir.join("config.acl");
test_config(&cfg);
let agent = a3s_code_core::Agent::new(cfg.to_string_lossy().to_string())
.await
.unwrap();
let llm = Arc::new(CaptureLlmClient::new(vec![
tool_call_response("web_fetch", serde_json::json!({"url": "not-a-url"})),
done_response(),
]));
let confirmation = a3s_code_core::hitl::ConfirmationPolicy::enabled()
.with_timeout(300, TimeoutAction::Reject);
let opts = tui_session_options(confirmation)
.with_llm_client(llm)
.with_planning_mode(a3s_code_core::PlanningMode::Disabled);
let session = agent
.session(dir.to_string_lossy().to_string(), Some(opts))
.unwrap();
let (mut rx, join) = session
.stream("Fetch a URL for research.", None)
.await
.unwrap();
let mut saw_fetch_end = None;
while let Some(event) = rx.recv().await {
match event {
a3s_code_core::AgentEvent::ToolEnd {
name,
output,
exit_code,
..
} if name == "web_fetch" => {
saw_fetch_end = Some((output, exit_code));
}
a3s_code_core::AgentEvent::PermissionDenied {
tool_name, reason, ..
} => panic!("{tool_name} was denied: {reason}"),
a3s_code_core::AgentEvent::End { .. } => break,
a3s_code_core::AgentEvent::Error { message } => panic!("{message}"),
_ => {}
}
}
join.await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
let (output, exit_code) = saw_fetch_end.expect("web_fetch should run");
assert_ne!(exit_code, 0, "invalid URL should fail validation");
assert!(
!output.contains("Permission denied"),
"web_fetch should not be blocked by permission policy: {output}"
);
}
#[tokio::test]
async fn hitl_wait_does_not_consume_tool_timeout_budget() {
let dir = std::env::temp_dir().join(format!(
"a3s-hitl-timeout-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let cfg = dir.join("config.acl");
test_config(&cfg);
std::fs::write(dir.join("sample.txt"), "timeout sentinel").unwrap();
let agent = a3s_code_core::Agent::new(cfg.to_string_lossy().to_string())
.await
.unwrap();
let llm = Arc::new(CaptureLlmClient::new(vec![
tool_call_response("read", serde_json::json!({"file_path": "sample.txt"})),
done_response(),
]));
let confirmation = a3s_code_core::hitl::ConfirmationPolicy::enabled()
.with_timeout(5_000, TimeoutAction::Reject);
let opts = tui_session_options(confirmation)
.with_tool_timeout(300)
.with_llm_client(llm)
.with_permission_policy(a3s_code_core::permissions::PermissionPolicy::new())
.with_planning_mode(a3s_code_core::PlanningMode::Disabled);
let session = agent
.session(dir.to_string_lossy().to_string(), Some(opts))
.unwrap();
let (mut rx, join) = session.stream("Read sample.txt.", None).await.unwrap();
let mut saw_confirmation = false;
let mut tool_output = None;
while let Some(event) = rx.recv().await {
match event {
a3s_code_core::AgentEvent::ConfirmationRequired { tool_id, .. } => {
saw_confirmation = true;
tokio::time::sleep(Duration::from_millis(500)).await;
assert!(session
.confirm_tool_use(&tool_id, true, None)
.await
.unwrap());
}
a3s_code_core::AgentEvent::ToolEnd {
output, exit_code, ..
} => {
assert_eq!(exit_code, 0, "{output}");
assert!(!output.contains("timed out"), "{output}");
tool_output = Some(output);
}
a3s_code_core::AgentEvent::End { .. } => break,
a3s_code_core::AgentEvent::Error { message } => panic!("{message}"),
_ => {}
}
}
join.await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
assert!(saw_confirmation, "the tool call should require HITL");
assert!(
tool_output
.as_deref()
.is_some_and(|output| output.contains("timeout sentinel")),
"tool output should come from read, got {tool_output:?}"
);
}
#[tokio::test]
async fn claude_session_surface_passes_system_tools_and_skills_to_llm() {
let dir = std::env::temp_dir().join(format!(
"a3s-claude-surface-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let cfg = dir.join("config.acl");
test_config(&cfg);
std::fs::write(
dir.join("CLAUDE.md"),
"Project rule: claude-session-surface-marker",
)
.unwrap();
let skill_dir = dir.join(".claude/skills/inspect-surface");
std::fs::create_dir_all(&skill_dir).unwrap();
std::fs::write(
skill_dir.join("SKILL.md"),
"---\nname: inspect-surface\n\
description: Inspect the Claude session surface\n\
kind: instruction\n\
allowed-tools:\n - Read\n---\n\
Use this skill marker: inspect-surface-skill-marker\n",
)
.unwrap();
let agent = a3s_code_core::Agent::new(cfg.to_string_lossy().to_string())
.await
.unwrap();
let llm = Arc::new(CaptureLlmClient::new(vec![done_response()]));
let opts = SessionOptions::new()
.with_llm_client(llm.clone())
.with_prompt_slots(
SystemPromptSlots::default()
.with_extra(project_instructions(dir.to_str().unwrap()).unwrap()),
)
.with_skill_dirs(agent_skill_dirs(dir.to_str().unwrap()))
.with_manual_delegation_enabled(true)
.with_auto_delegation_enabled(false)
.with_planning_mode(a3s_code_core::PlanningMode::Disabled);
let session = agent
.session(dir.to_string_lossy().to_string(), Some(opts))
.unwrap();
let (mut rx, join) = session
.stream("Use available skills to inspect this project.", None)
.await
.unwrap();
while let Some(event) = rx.recv().await {
if matches!(event, a3s_code_core::AgentEvent::End { .. }) {
break;
}
}
join.await.unwrap();
let turns = llm.turns();
let captured = turns.first().unwrap();
let system = captured.system.as_deref().unwrap();
let _ = std::fs::remove_dir_all(&dir);
assert!(
system.contains("You are A3S Code"),
"core system prompt should reach the LLM"
);
assert!(
system.contains("claude-session-surface-marker"),
"CLAUDE.md project instructions should reach the LLM"
);
assert!(
system.contains("# Skills"),
"skill catalog guidance should reach the LLM system prompt"
);
assert!(
captured.tools.iter().any(|name| name == "read")
&& captured.tools.iter().any(|name| name == "Skill")
&& captured.tools.iter().any(|name| name == "search_skills")
&& captured.tools.iter().any(|name| name == "parallel_task"),
"a3s tools and skill tools should be model-visible; got {:?}",
captured.tools
);
}
#[tokio::test]
async fn claude_can_invoke_skill_and_child_run_receives_skill_prompt() {
let dir = std::env::temp_dir().join(format!(
"a3s-claude-skill-invoke-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let cfg = dir.join("config.acl");
test_config(&cfg);
let skill_dir = dir.join(".claude/skills/inspect-surface");
std::fs::create_dir_all(&skill_dir).unwrap();
std::fs::write(
skill_dir.join("SKILL.md"),
"---\nname: inspect-surface\n\
description: Inspect the Claude session surface\n\
kind: instruction\n\
allowed-tools:\n - Read\n---\n\
Use this skill marker: inspect-surface-skill-marker\n",
)
.unwrap();
let agent = a3s_code_core::Agent::new(cfg.to_string_lossy().to_string())
.await
.unwrap();
let llm = Arc::new(CaptureLlmClient::new(vec![
tool_call_response(
"Skill",
serde_json::json!({
"skill_name": "inspect-surface",
"prompt": "Apply the inspect-surface skill."
}),
),
done_response(),
done_response(),
]));
let opts = SessionOptions::new()
.with_llm_client(llm.clone())
.with_skill_dirs(agent_skill_dirs(dir.to_str().unwrap()))
.with_manual_delegation_enabled(true)
.with_auto_delegation_enabled(false)
.with_permission_policy(
a3s_code_core::permissions::PermissionPolicy::new().allow("Skill(*)"),
)
.with_planning_mode(a3s_code_core::PlanningMode::Disabled)
.with_max_tool_rounds(5);
let session = agent
.session(dir.to_string_lossy().to_string(), Some(opts))
.unwrap();
let result = session
.send("Use the inspect-surface skill.", None)
.await
.unwrap();
let turns = llm.turns();
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(result.text.trim(), "DONE");
let system_snippets = turns
.iter()
.enumerate()
.map(|(index, turn)| {
format!(
"#{index}: {}",
turn.system
.as_deref()
.unwrap_or("<none>")
.chars()
.take(220)
.collect::<String>()
)
})
.collect::<Vec<_>>()
.join("\n");
assert!(
turns
.iter()
.any(|turn| turn.system.as_deref().is_some_and(|system| {
system.contains("You are executing the 'inspect-surface' skill")
&& system.contains("inspect-surface-skill-marker")
})),
"Skill tool should start a child LLM run with the skill prompt; turns: {}",
system_snippets
);
}
#[test]
fn workflow_doc_captures_single_task_dispatch() {
let args = serde_json::json!({
"agent": "plan",
"description": "Design the rendering architecture",
"prompt": "Plan a layered renderer."
});
let (doc, label) = workflow_doc_for_tool("task", Some(&args)).unwrap();
assert!(label.contains("delegated task"), "{label}");
assert!(doc.contains("Design the rendering architecture"));
assert!(doc.contains("Agent: `plan`"));
assert!(doc.contains("Plan a layered renderer."));
}
#[test]
fn workflow_doc_captures_dynamic_workflow_tool_without_flow_command() {
let args = serde_json::json!({
"source": "async function run(ctx, inputs) { return { type: 'complete', output: inputs }; }"
});
let (doc, label) = workflow_doc_for_tool("dynamic_workflow", Some(&args)).unwrap();
assert!(
label.contains("dynamic workflow script captured"),
"{label}"
);
assert!(!label.contains("/flow"), "{label}");
assert!(doc.contains("async function run"));
}
#[test]
fn synthesis_requires_activity_without_followup_text() {
assert!(needs_synthesis(false, false, true, false));
assert!(!needs_synthesis(false, false, true, true));
assert!(!needs_synthesis(false, true, true, false));
assert!(!needs_synthesis(false, false, false, false));
assert!(!needs_synthesis(true, false, true, false));
}
#[test]
fn estimate_tokens_counts_wide_unicode_heavier_than_ascii() {
assert_eq!(estimate_tokens("abcd"), 1); assert_eq!(estimate_tokens("かなテストあ"), 6); assert_eq!(estimate_tokens("hi かな"), 2); assert_eq!(estimate_tokens(""), 0);
}
#[test]
fn ctx_limit_falls_back_when_undeclared() {
assert_eq!(resolve_ctx_limit(Some(200_000)), 200_000); assert_eq!(resolve_ctx_limit(Some(0)), DEFAULT_CONTEXT_LIMIT); assert_eq!(resolve_ctx_limit(None), DEFAULT_CONTEXT_LIMIT); }
#[test]
fn ctx_limit_prefers_declared_then_infers_account_models() {
let mut ctx = std::collections::HashMap::new();
ctx.insert("openai/gpt-5".to_string(), 256_000);
assert_eq!(ctx_limit_for_model(&ctx, "openai/gpt-5"), 256_000);
assert_eq!(inferred_ctx_limit("claude-sonnet-4-6"), Some(200_000));
assert_eq!(inferred_ctx_limit("claude-opus-4-8[1m]"), Some(1_000_000));
assert_eq!(inferred_ctx_limit("gpt-4.1"), Some(1_000_000));
assert_eq!(inferred_ctx_limit("glm-5.1"), Some(DEFAULT_CONTEXT_LIMIT));
assert_eq!(
ctx_limit_for_model(&ctx, "unknown-model"),
DEFAULT_CONTEXT_LIMIT
);
}
#[test]
fn auto_compact_threshold_scales_to_real_window() {
assert!((auto_compact_threshold_for(128_000) - 0.544).abs() < 0.001);
assert!((auto_compact_threshold_for(200_000) - 0.85).abs() < 0.001);
assert_eq!(auto_compact_threshold_for(1_000_000), 1.0);
assert!((auto_compact_threshold_for(0) - 0.85).abs() < 0.001);
assert!((auto_compact_threshold_for(8_000) - 0.034).abs() < 0.001);
}
#[test]
fn ctx_warn_tier_latches_once_and_rearms_on_drop() {
assert_eq!(ctx_warn_tier(40, 0), (0, None));
assert_eq!(ctx_warn_tier(72, 0), (70, Some(70)));
assert_eq!(ctx_warn_tier(79, 70), (70, None)); assert_eq!(ctx_warn_tier(91, 70), (85, Some(85)));
assert_eq!(ctx_warn_tier(100, 85), (85, None));
assert_eq!(ctx_warn_tier(30, 85), (0, None));
assert_eq!(ctx_warn_tier(72, 0), (70, Some(70)));
assert_eq!(ctx_warn_tier(90, 0), (85, Some(85)));
}
#[test]
fn task_tool_empty_child_output_renders_useful_summary() {
let args = serde_json::json!({
"agent": "plan",
"description": "Plan subsystem boundaries",
"prompt": "Create the plan."
});
let meta = serde_json::json!({
"task_id": "task-abc123",
"session_id": "task-run-task-abc123",
"agent": "plan",
"success": true,
"output_bytes": 0,
"artifact_uri": "a3s://tasks/task-run-task-abc123/runs/task-abc123/output"
});
let output = "Task completed: task-abc123\n\
Agent: plan\n\
Session: task-run-task-abc123\n\
Task ID: task-abc123\n\
Artifact ID: task-output:task-abc123\n\
Artifact URI: a3s://tasks/task-run-task-abc123/runs/task-abc123/output\n\
Output:\n";
let out = render_tool_end("task", 0, output, Some(&meta), Some(&args), 100);
let plain = strip_ansi(&out);
assert!(plain.contains("Explored"));
assert!(plain.contains("Task completed · plan · task-abc123"));
assert!(plain.contains("no child text output"));
assert!(plain.contains("artifact: a3s://tasks/task-run-task-abc123"));
}
#[test]
fn edit_metadata_renders_colored_diff() {
let meta = serde_json::json!({
"file_path": "src/x.rs",
"before": "let a = 1;\nkeep;\n",
"after": "let a = 2;\nkeep;\n",
});
let out = render_tool_end("edit", 0, "ok", Some(&meta), None, 80);
let plain = strip_ansi(&out);
assert!(plain.contains("src/x.rs"), "header has path");
assert!(
plain.contains("+1") && plain.contains("-1"),
"add/del counts"
);
assert!(plain.contains("let a = 2;"), "shows inserted line");
assert!(plain.contains("let a = 1;"), "shows deleted line");
assert!(
plain.contains("keep;"),
"context lines are shown (unified diff)"
);
assert!(plain.contains("Edited src/x.rs"), "edit header with path");
}
fn strip_ansi(s: &str) -> String {
let mut out = String::new();
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\x1b' {
for c2 in chars.by_ref() {
if c2 == 'm' {
break;
}
}
} else {
out.push(c);
}
}
out
}
#[test]
fn non_edit_tool_renders_status_line() {
let out = render_tool_end("bash", 0, "hello\nworld", None, None, 80);
assert!(out.contains("Ran") && out.contains("hello"));
assert!(!out.contains('✎'), "no diff marker for non-edit tools");
}
#[test]
fn tool_end_shows_primary_arg_summary() {
let args = serde_json::json!({ "command": "npm test", "timeout": 60 });
let out = render_tool_end("bash", 0, "ok\n", None, Some(&args), 80);
let plain = a3s_tui::style::strip_ansi(&out);
assert!(plain.contains("Ran"), "action verb for bash");
assert!(plain.contains("npm test"), "shows the command argument");
}
#[test]
fn arg_summary_extracts_known_keys() {
assert_eq!(
arg_summary(&serde_json::json!({ "command": "ls -la" })),
Some("ls -la".to_string())
);
assert_eq!(
arg_summary(&serde_json::json!({ "pattern": "TODO" })),
Some("TODO".to_string())
);
assert_eq!(arg_summary(&serde_json::json!({ "unknown": "x" })), None);
}
#[test]
fn slash_tail_requires_a_token_boundary() {
let parameterized = [
"/login", "/ctx", "/kb", "/okf", "/btw", "/goal", "/loop", "/sleep", "/flow", "/agent",
"/mcp", "/skill",
];
for cmd in parameterized {
assert_eq!(slash_tail(cmd, cmd), Some(""), "{cmd} accepts bare form");
assert_eq!(
slash_tail(&format!("{cmd} argument"), cmd),
Some(" argument"),
"{cmd} accepts whitespace-delimited arguments"
);
assert_eq!(
slash_tail(&format!("{cmd}x"), cmd),
None,
"{cmd}x must remain a normal message, not {cmd}"
);
assert_eq!(
slash_tail(&format!("{cmd}-token"), cmd),
None,
"{cmd}-token must remain a normal message, not {cmd}"
);
}
}
#[test]
fn cloned_asset_focus_matches_only_paths_inside_the_clone_root() {
let clone_root = std::path::Path::new("/tmp/a3s-assets/weather-agent");
assert!(App::path_is_within(clone_root, clone_root));
assert!(App::path_is_within(
std::path::Path::new("/tmp/a3s-assets/weather-agent/agent.md"),
clone_root
));
assert!(App::path_is_within(
std::path::Path::new("/tmp/a3s-assets/weather-agent/nested/asset.json"),
clone_root
));
assert!(!App::path_is_within(
std::path::Path::new("/tmp/a3s-assets/weather-agent-2/agent.md"),
clone_root
));
}
#[test]
fn runtime_asset_query_carries_asset_category_and_terms() {
assert_eq!(
runtime_asset_query("mcp", "Calc Tools", "failed calls"),
"category:mcp Calc Tools failed calls"
);
assert_eq!(
runtime_asset_query("workflow", "daily-flow", ""),
"category:workflow daily-flow"
);
assert_eq!(runtime_asset_query("", "", "stale"), "stale");
}
#[test]
fn slash_command_registry_is_unique_english_and_idle_safe() {
let mut seen = HashSet::new();
for (cmd, desc) in SLASH_COMMANDS {
assert!(cmd.starts_with('/'), "{cmd} should be a slash command");
assert!(
!cmd.contains(char::is_whitespace),
"{cmd} should be the bare command token"
);
assert!(seen.insert(*cmd), "{cmd} should not be registered twice");
assert!(
!desc.trim().is_empty(),
"{cmd} should have a menu description"
);
assert!(
!contains_cjk(desc),
"{cmd} description should stay English-only: {desc}"
);
assert!(
!desc.to_ascii_lowercase().contains("repo"),
"{cmd} slash-menu copy should not expose asset-workspace management: {desc}"
);
}
for cmd in IDLE_ONLY {
assert!(
SLASH_COMMANDS
.iter()
.any(|(registered, _)| registered == cmd),
"{cmd} is idle-only but missing from the slash registry"
);
}
let removed_commands = [
"im", "run", "deploy", "review", "list", "ps", "workflow", "repo", "git", "relay",
]
.into_iter()
.map(|name| format!("/{name}"))
.chain([
format!("/{}{}", "evo", "lve"),
format!("/{}{}", "evo", "love"),
]);
for removed in removed_commands {
assert!(
!SLASH_COMMANDS
.iter()
.any(|(cmd, _)| *cmd == removed.as_str()),
"{removed} should stay removed from the slash registry"
);
}
}
#[test]
fn asset_root_commands_are_backed_by_lifecycle_services() {
let asset_commands: HashSet<&str> = asset_lifecycle::ASSET_LIFECYCLES
.iter()
.map(|lifecycle| lifecycle.command)
.collect();
assert_eq!(
asset_commands,
HashSet::from(["/agent", "/mcp", "/skill", "/okf", "/flow"])
);
for command in asset_commands {
let menu_desc = SLASH_COMMANDS
.iter()
.find_map(|(cmd, desc)| (*cmd == command).then_some(*desc))
.unwrap_or_else(|| panic!("{command} should be registered in the slash menu"));
let services: HashSet<&str> = asset_lifecycle::ASSET_LIFECYCLES
.iter()
.filter(|lifecycle| lifecycle.command == command)
.map(|lifecycle| asset_lifecycle::service_label(lifecycle.service))
.collect();
for service in services {
assert!(
menu_desc.contains(service),
"{command} slash-menu copy should name {service}: {menu_desc}"
);
}
assert!(
!menu_desc.contains("lifecycle"),
"{command} slash-menu copy should name concrete OS services, not generic lifecycle wording: {menu_desc}"
);
}
}
#[test]
fn cancel_pending_picker_clears_panel_and_deferred_asset_command() {
let mut picker = Some("agent selector");
let mut pending = Some("review");
cancel_pending_picker(&mut picker, &mut pending);
assert!(picker.is_none());
assert!(pending.is_none());
}
#[test]
fn registered_slash_commands_have_declared_handler_paths() {
let parameterized = HashSet::from([
"/login", "/ctx", "/kb", "/okf", "/btw", "/goal", "/loop", "/sleep", "/flow", "/agent",
"/mcp", "/skill",
]);
let exact = HashSet::from([
"/logout", "/exit", "/fork", "/clear", "/init", "/compact", "/help", "/view", "/auto",
"/config", "/model", "/effort", "/top", "/ide", "/plugin", "/theme", "/output",
"/reload", "/update", "/memory",
]);
for (cmd, _) in SLASH_COMMANDS {
assert!(
parameterized.contains(cmd) || exact.contains(cmd),
"{cmd} is registered but not mapped to a handler category"
);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SlashHandlerKind {
Exact,
Parameterized,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SlashRuntimeScope {
Local,
OsAccount,
RuntimeConditional,
}
#[derive(Clone, Copy, Debug)]
struct SlashAuditRow {
command: &'static str,
handler: SlashHandlerKind,
idle_only: bool,
scope: SlashRuntimeScope,
}
fn slash_audit_rows() -> Vec<SlashAuditRow> {
use SlashHandlerKind::{Exact, Parameterized};
use SlashRuntimeScope::{Local, OsAccount, RuntimeConditional};
vec![
SlashAuditRow {
command: "/model",
handler: Exact,
idle_only: true,
scope: OsAccount,
},
SlashAuditRow {
command: "/init",
handler: Exact,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/config",
handler: Exact,
idle_only: false,
scope: Local,
},
SlashAuditRow {
command: "/theme",
handler: Exact,
idle_only: false,
scope: Local,
},
SlashAuditRow {
command: "/flow",
handler: Parameterized,
idle_only: true,
scope: OsAccount,
},
SlashAuditRow {
command: "/agent",
handler: Parameterized,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/mcp",
handler: Parameterized,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/skill",
handler: Parameterized,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/okf",
handler: Parameterized,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/output",
handler: Exact,
idle_only: false,
scope: Local,
},
SlashAuditRow {
command: "/login",
handler: Parameterized,
idle_only: false,
scope: OsAccount,
},
SlashAuditRow {
command: "/logout",
handler: Exact,
idle_only: false,
scope: OsAccount,
},
SlashAuditRow {
command: "/view",
handler: Exact,
idle_only: false,
scope: OsAccount,
},
SlashAuditRow {
command: "/plugin",
handler: Exact,
idle_only: false,
scope: Local,
},
SlashAuditRow {
command: "/reload",
handler: Exact,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/update",
handler: Exact,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/btw",
handler: Parameterized,
idle_only: false,
scope: Local,
},
SlashAuditRow {
command: "/top",
handler: Exact,
idle_only: false,
scope: Local,
},
SlashAuditRow {
command: "/ide",
handler: Exact,
idle_only: false,
scope: Local,
},
SlashAuditRow {
command: "/memory",
handler: Exact,
idle_only: false,
scope: Local,
},
SlashAuditRow {
command: "/kb",
handler: Parameterized,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/ctx",
handler: Parameterized,
idle_only: false,
scope: Local,
},
SlashAuditRow {
command: "/effort",
handler: Exact,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/compact",
handler: Exact,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/goal",
handler: Parameterized,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/loop",
handler: Parameterized,
idle_only: true,
scope: RuntimeConditional,
},
SlashAuditRow {
command: "/sleep",
handler: Parameterized,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/help",
handler: Exact,
idle_only: false,
scope: Local,
},
SlashAuditRow {
command: "/fork",
handler: Exact,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/clear",
handler: Exact,
idle_only: true,
scope: Local,
},
SlashAuditRow {
command: "/auto",
handler: Exact,
idle_only: false,
scope: Local,
},
SlashAuditRow {
command: "/exit",
handler: Exact,
idle_only: false,
scope: Local,
},
]
}
#[test]
fn slash_command_audit_matrix_matches_registry_and_policies() {
let rows = slash_audit_rows();
let registered = SLASH_COMMANDS
.iter()
.map(|(cmd, _)| *cmd)
.collect::<HashSet<_>>();
let audited = rows.iter().map(|row| row.command).collect::<HashSet<_>>();
assert_eq!(
registered, audited,
"every registered command must have explicit audit metadata"
);
let idle_from_rows = rows
.iter()
.filter(|row| row.idle_only)
.map(|row| row.command)
.collect::<HashSet<_>>();
let idle_from_const = IDLE_ONLY.iter().copied().collect::<HashSet<_>>();
assert_eq!(
idle_from_rows, idle_from_const,
"idle-only policy should stay in sync with the audit matrix"
);
let parameterized_names = HashSet::from([
"/login", "/ctx", "/kb", "/okf", "/btw", "/goal", "/loop", "/sleep", "/flow", "/agent",
"/mcp", "/skill",
]);
for row in &rows {
match row.handler {
SlashHandlerKind::Parameterized => {
assert!(
parameterized_names.contains(row.command),
"{} should be in the token-boundary handler set",
row.command
);
assert!(
slash_tail(row.command, row.command).is_some(),
"{} should be token-boundary parsed",
row.command
);
}
SlashHandlerKind::Exact => {
assert!(
!parameterized_names.contains(row.command),
"{} exact command should not be in the token-boundary handler set",
row.command
);
}
}
}
let loop_row = rows.iter().find(|row| row.command == "/loop").unwrap();
assert_eq!(loop_row.scope, SlashRuntimeScope::RuntimeConditional);
for cmd in ["/agent", "/mcp", "/skill", "/okf", "/kb", "/ctx"] {
let row = rows.iter().find(|row| row.command == cmd).unwrap();
assert_eq!(row.scope, SlashRuntimeScope::Local);
}
}
#[test]
fn removed_top_level_aliases_stay_unregistered() {
let removed = [
"/mouse".to_string(),
"/plugins".to_string(),
"/quit".to_string(),
format!("/{}{}", "re", "po"),
format!("/{}{}", "re", "lay"),
];
for alias in removed {
assert!(
!SLASH_COMMANDS.iter().any(|(cmd, _)| *cmd == alias.as_str()),
"{alias} should stay removed from the slash registry"
);
}
}
#[test]
fn ampersand_clone_review_syntax_stays_removed() {
assert!(
slash_candidates("&").is_empty(),
"asset clone shortcuts must not return to the slash menu"
);
assert!(
!SLASH_COMMANDS.iter().any(|(cmd, _)| cmd.starts_with('&')),
"asset clone/review flows must stay under typed asset subcommands"
);
}
#[test]
fn reload_is_idle_only_because_it_rebuilds_the_session() {
assert!(IDLE_ONLY.contains(&"/reload"));
}
#[test]
fn fork_is_idle_only_and_listed() {
assert!(IDLE_ONLY.contains(&"/fork"));
assert!(SLASH_COMMANDS.iter().any(|(name, _)| *name == "/fork"));
}
#[test]
fn asset_workflow_commands_are_idle_only_and_listed() {
for cmd in ["/flow", "/agent", "/mcp", "/skill", "/okf"] {
assert!(
IDLE_ONLY.contains(&cmd),
"{cmd} must not arm asset workflows while another turn is running"
);
assert!(
SLASH_COMMANDS.iter().any(|(name, _)| *name == cmd),
"{cmd} should be visible in the slash menu while idle"
);
}
}
#[test]
fn runtime_activity_are_asset_scoped_not_top_level() {
let top_level_ps = format!("/{}", "ps");
assert!(
!SLASH_COMMANDS
.iter()
.any(|(name, _)| *name == top_level_ps.as_str()),
"runtime activity browsing should stay asset-scoped"
);
assert!(matches!(
panels::agent::parse_agent_subcommand("activity")
.unwrap()
.unwrap(),
panels::agent::AgentSubcommand::Activity(_)
));
assert!(panels::agent::parse_agent_subcommand("ps")
.unwrap()
.is_err());
assert!(matches!(
panels::mcp::parse_mcp_subcommand("activity")
.unwrap()
.unwrap(),
panels::mcp::McpSubcommand::Activity(_)
));
assert!(panels::mcp::parse_mcp_subcommand("ps").unwrap().is_err());
assert!(matches!(
panels::flow::parse_flow_subcommand("activity")
.unwrap()
.unwrap(),
panels::flow::FlowSubcommand::Activity(_)
));
assert!(panels::flow::parse_flow_subcommand("ps").unwrap().is_err());
assert!(matches!(
panels::skill::parse_skill_subcommand("activity")
.unwrap()
.unwrap(),
panels::skill::SkillSubcommand::Activity(_)
));
assert!(panels::skill::parse_skill_subcommand("ps")
.unwrap()
.is_err());
assert!(matches!(
panels::okf::parse_okf_command("activity"),
panels::okf::OkfCommand::Activity(_)
));
assert!(matches!(
panels::okf::parse_okf_command("ps"),
panels::okf::OkfCommand::Usage(_)
));
}
#[test]
fn runtime_expectation_warns_once_until_evidence_arrives() {
let mut missing = RuntimeExpectation::required("deep research");
let warning = missing.missing_warning().unwrap();
assert!(warning.contains("Runtime evidence missing"), "{warning}");
assert!(missing.missing_warning().is_none());
let mut via_runtime = RuntimeExpectation::required("run");
via_runtime.record_tool("runtime");
assert!(via_runtime.is_satisfied());
assert!(via_runtime.missing_warning().is_none());
let mut via_parallel = RuntimeExpectation::required("review");
via_parallel.record_tool("parallel_task");
assert!(via_parallel.is_satisfied());
let mut via_dynamic_workflow = RuntimeExpectation::required("research");
via_dynamic_workflow.record_tool("dynamic_workflow");
assert!(via_dynamic_workflow.is_satisfied());
let mut via_view = RuntimeExpectation::required("deploy");
via_view.record_remote_view();
assert!(via_view.is_satisfied());
let mut report_only_runtime = RuntimeExpectation::required_report_view("deep research");
report_only_runtime.record_tool("runtime");
assert!(!report_only_runtime.is_satisfied());
let warning = report_only_runtime.missing_warning().unwrap();
assert!(warning.contains("report"), "{warning}");
assert!(warning.contains(".view"), "{warning}");
let correction = report_only_runtime.corrective_prompt().unwrap();
assert!(correction.contains("deep research"), "{correction}");
assert!(correction.contains("dynamic_workflow"), "{correction}");
assert!(correction.contains("OS Runtime"), "{correction}");
assert!(correction.contains(".view"), "{correction}");
assert!(correction.contains("viewUrl"), "{correction}");
let mut report_only_view = RuntimeExpectation::required_report_view("loop daily-triage");
report_only_view.record_remote_view();
assert!(!report_only_view.is_satisfied());
let warning = report_only_view.missing_warning().unwrap();
assert!(warning.contains("fan-out"), "{warning}");
let correction = report_only_view.corrective_prompt().unwrap();
assert!(correction.contains("fan-out"), "{correction}");
let mut full_report = RuntimeExpectation::required_report_view("deep research");
full_report.record_tool("dynamic_workflow");
full_report.record_remote_view();
assert!(full_report.is_satisfied());
assert!(full_report.missing_warning().is_none());
assert!(full_report.corrective_prompt().is_none());
}
#[test]
fn remote_view_detection_only_marks_new_specs() {
let spec = remote_ui::ViewSpec {
url: "https://os.example.com/admin/runtime/jobs/1?embed=1".into(),
width: Some(1200),
height: Some(800),
embeddable: true,
};
assert!(is_new_remote_view(None, &spec));
assert!(!is_new_remote_view(Some(&spec), &spec));
}
#[test]
fn os_required_message_distinguishes_missing_config_from_missing_login() {
let configured = os_required_message("/agent run", true);
assert!(configured.contains("/login"));
assert!(!configured.contains("configure `os"));
let missing = os_required_message("/agent deploy", false);
assert!(missing.contains("configure `os"));
assert!(missing.contains("/login"));
}
#[test]
fn os_required_alert_uses_shared_warning_line() {
let rendered = os_required_alert("/agent run", true);
assert_eq!(
a3s_tui::style::strip_ansi(&rendered),
" ⚠ /agent run needs OS — sign in with /login first"
);
assert!(rendered.contains("\x1b[38;2;245;166;35m"));
}
#[test]
fn ide_flash_line_uses_shared_toast_component() {
let rendered = ide_flash_line(ToastKind::Warning, "read-only");
assert_eq!(a3s_tui::style::strip_ansi(&rendered), "⚠ read-only");
assert!(rendered.contains("\x1b[38;2;245;166;35m"));
}
#[test]
fn image_path_detection() {
assert!(is_image_path(std::path::Path::new("a.PNG")));
assert!(is_image_path(std::path::Path::new("x/y.jpeg")));
assert!(!is_image_path(std::path::Path::new("main.rs")));
assert!(!is_image_path(std::path::Path::new("noext")));
}
#[test]
fn half_block_render_packs_two_rows_and_colors() {
let img = ::image::DynamicImage::ImageRgba8(::image::RgbaImage::from_pixel(
4,
6,
::image::Rgba([10, 20, 30, 255]),
));
let lines = render_image_blocks(&img, 80, 40);
assert_eq!(lines.len(), 3, "6px / 2 = 3 rows");
assert!(lines[0].contains('▀'), "uses upper half-block");
assert!(lines[0].contains("\x1b["), "carries ANSI color");
}
#[test]
fn half_block_render_fits_within_bounds() {
let img = ::image::DynamicImage::ImageRgba8(::image::RgbaImage::new(400, 400));
let lines = render_image_blocks(&img, 20, 10);
assert!(lines.len() <= 10, "never exceeds max_rows");
}
#[test]
fn clipboard_helper_cleans_up_on_no_image() {
let dest = std::env::temp_dir().join("a3s-test-noimg.png");
let _ = std::fs::remove_file(&dest);
let ok = clipboard_image_to(&dest);
if !ok {
assert!(!dest.exists(), "failed paste leaves no file");
} else {
let _ = std::fs::remove_file(&dest);
}
}
#[test]
fn char_byte_handles_ascii_and_wide_unicode() {
assert_eq!(char_byte("hello", 0), 0);
assert_eq!(char_byte("hello", 3), 3);
assert_eq!(char_byte("hello", 5), 5); assert_eq!(char_byte("あい", 1), 3);
assert_eq!(char_byte("あい", 2), 6);
}
#[test]
fn char_byte_supports_inplace_edits() {
let mut s = String::from("ab");
let b = char_byte(&s, 1);
s.insert(b, 'あ');
assert_eq!(s, "aあb");
}
#[test]
fn starter_config_template_parses() {
let p = std::env::temp_dir().join("a3s-template-test.acl");
std::fs::write(&p, config_template()).unwrap();
let cfg = a3s_code_core::config::CodeConfig::from_file(&p)
.expect("starter template must parse as valid ACL");
let models: Vec<_> = cfg.list_models().into_iter().collect();
assert!(!models.is_empty(), "template defines at least one model");
let _ = std::fs::remove_file(&p);
}
#[test]
fn counts_skill_dirs_and_flat_md() {
let base = std::env::temp_dir().join("a3s-skillcount-test");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(base.join("myskill")).unwrap();
std::fs::write(base.join("myskill/SKILL.md"), "# skill").unwrap();
std::fs::write(base.join("flat.md"), "# flat skill").unwrap();
std::fs::write(base.join("notes.txt"), "ignored").unwrap();
assert_eq!(count_skill_files(std::slice::from_ref(&base)), 2);
let _ = std::fs::remove_dir_all(&base);
}
}