From 7622c8a1b2fb7a88918d081effd68d66072b7e2a Mon Sep 17 00:00:00 2001 From: Roy Lin Date: Wed, 1 Jul 2026 08:01:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20rename=20RemoteUI=20link=20?= =?UTF-8?q?=E6=89=93=E5=BC=80=E6=B8=90=E8=BF=9B=E5=BC=8FUI=20=E2=86=92=20?= =?UTF-8?q?=E6=9F=A5=E7=9C=8B=E8=A7=86=E5=9B=BE=20(v0.5.14)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 2 +- Cargo.toml | 2 +- skills/a3s-os-capabilities.md | 4 ++-- src/tui/mod.rs | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 59109f5..d0b5c7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "a3s" -version = "0.5.13" +version = "0.5.14" dependencies = [ "a3s-code-core", "a3s-tui", diff --git a/Cargo.toml b/Cargo.toml index 1af2e5c..3a4528b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "a3s" -version = "0.5.13" +version = "0.5.14" edition = "2021" description = "a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI" license = "MIT" diff --git a/skills/a3s-os-capabilities.md b/skills/a3s-os-capabilities.md index 4d948fa..8bc87b6 100644 --- a/skills/a3s-os-capabilities.md +++ b/skills/a3s-os-capabilities.md @@ -96,8 +96,8 @@ signed-in user may access. do **not** narrow it away with `jq`; emit the full response or keep it in your projection (e.g. `... | jq '{ data, view }'`). Never fabricate or drop a returned `view`. - 2. **End your reply with the link on its own line, exactly:** `🔗 打开渐进式UI` - — the host turns any reply line containing `打开渐进式UI` into a one-click + 2. **End your reply with the link on its own line, exactly:** `🔗 查看视图` + — the host turns any reply line containing `查看视图` into a one-click trigger that opens the view in the authenticated **渐进式UI** popup (the user's current OS login is injected — no re-login). RemoteUI is **user-triggered**: the popup is NOT opened automatically; the user clicks diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 2d2f71f..7ed740d 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -95,7 +95,7 @@ Pipe responses through `jq` to extract ONLY the fields you need (e.g. \ `| jq -r '.data.modules[].name'`) so output stays a few lines; summarize the result for the user \ in a few lines and do NOT paste the whole raw JSON back. \ If a response contains a `view` object (a console page sized for a popup), keep `.view` in your \ -JSON output and END your reply with the link on its own line, exactly `🔗 打开渐进式UI` — the host \ +JSON output and END your reply with the link on its own line, exactly `🔗 查看视图` — the host \ turns it into a one-click trigger that opens the authenticated 渐进式UI popup (the user's OS login \ is injected, no re-login). Do not print the raw URL. The `a3s-os-capabilities` skill has full examples." ) @@ -523,7 +523,7 @@ fn osc52_copy(text: &str) -> String { /// host recognises a mouse click on any reply line containing it and opens the /// remembered view (`/view` does the same). The link lives in the message text — /// the host renders no button of its own. -const VIEW_BUTTON_MARKER: &str = "打开渐进式UI"; +const VIEW_BUTTON_MARKER: &str = "查看视图"; /// Put `text` on the system clipboard: OSC 52 (portable, survives SSH on /// supporting terminals) plus the native tool where we have one (macOS pbcopy). @@ -1219,7 +1219,7 @@ struct App { /// gateway is unavailable/unconfigured. os_gateway_models: Option>, /// Last 书安OS view seen in a tool result. RemoteUI is user-triggered: `/view` - /// or clicking the agent's inline "打开渐进式UI" link opens it in the native + /// or clicking the agent's inline "查看视图" link opens it in the native /// a3s-webview window — it is never auto-opened. last_view: Option, /// Current model effort (index into EFFORT_LEVELS). @@ -1769,7 +1769,7 @@ impl Model for App { if let Some(s) = self.selection { if s.is_empty() { // A plain click: open the OS view if it landed on - // the agent's inline "打开渐进式UI" link; else just clear. + // the agent's inline "查看视图" link; else just clear. let view = self.viewport.view(); let clicked = a3s_tui::style::strip_ansi( view.split('\n') @@ -3348,7 +3348,7 @@ impl App { // open it now in the native a3s-webview window (auth via $A3S_OS_TOKEN). if let Some(spec) = remote_ui::find_view_url(&output) { // RemoteUI is user-triggered — never auto-open. Remember the - // view; the agent offers it via an inline "打开渐进式UI" link + // view; the agent offers it via an inline "查看视图" link // (clicking that line, or `/view`, opens the popup). self.last_view = Some(spec); } From 344d299648ed21d6e8e1d1f13b05f7ff0d3a666b Mon Sep 17 00:00:00 2001 From: Roy Lin Date: Wed, 1 Jul 2026 09:39:27 +0800 Subject: [PATCH 2/2] feat(tui): trace line (requestId + timestamp) for OS calls + Codex-style bash command coloring --- skills/a3s-os-capabilities.md | 14 +++++++++-- src/tui/mod.rs | 9 +++++-- src/tui/render.rs | 45 ++++++++++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/skills/a3s-os-capabilities.md b/skills/a3s-os-capabilities.md index 8bc87b6..64c7111 100644 --- a/skills/a3s-os-capabilities.md +++ b/skills/a3s-os-capabilities.md @@ -89,13 +89,17 @@ signed-in user may access. - Prefer read/`GET`-style operations for discovery; write operations (create / update / delete) run with the user's real platform permissions — confirm intent before mutating platform state. +- **Always report the call trace.** Every response envelope carries a `requestId` + and a `timestamp`. After summarizing the result, output them on their own line so + the call is traceable, exactly: `↳ requestId · `. Keep + `.requestId` and `.timestamp` in any `jq` projection (don't narrow them away). - **Offer the `view` as an inline link — never auto-open.** Some `execute` responses include a `view` object — `{ "url": "…?embed=1", "width": N, "height": N }` — a focused console page sized for a popup. Two things must happen: 1. **Keep `.view` in your command's JSON stdout** so the host can capture it — do **not** narrow it away with `jq`; emit the full response or keep it in - your projection (e.g. `... | jq '{ data, view }'`). Never fabricate or drop - a returned `view`. + your projection (e.g. `... | jq '{ data, view, requestId, timestamp }'`). + Never fabricate or drop a returned `view`. 2. **End your reply with the link on its own line, exactly:** `🔗 查看视图` — the host turns any reply line containing `查看视图` into a one-click trigger that opens the view in the authenticated **渐进式UI** popup (the @@ -103,6 +107,12 @@ signed-in user may access. **user-triggered**: the popup is NOT opened automatically; the user clicks that link (or runs `/view`). Do **not** print the raw URL yourself — the link line is the affordance. + + So a typical reply ends with two lines: + ``` + ↳ requestId 52178323-b614-42e4-af60-4b0b91ad8355 · 2026-07-01T01:21:07.905Z + 🔗 查看视图 + ``` - The `ui` field (`protocol: "agent-ui"`) is a host-rendered remote component — note that it exists if present, but don't try to render it yourself. diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 7ed740d..0066a17 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -94,6 +94,9 @@ Go broad→narrow: `list` (modules) → `describe`/`search` for the one operatio Pipe responses through `jq` to extract ONLY the fields you need (e.g. \ `| jq -r '.data.modules[].name'`) so output stays a few lines; summarize the result for the user \ in a few lines and do NOT paste the whole raw JSON back. \ +Every response envelope has a `requestId` and `timestamp`; after your summary, ALWAYS output them on \ +their own line, exactly `↳ requestId · `, so the call is traceable (keep \ +`.requestId`/`.timestamp` in any jq projection). \ If a response contains a `view` object (a console page sized for a popup), keep `.view` in your \ JSON output and END your reply with the link on its own line, exactly `🔗 查看视图` — the host \ turns it into a one-click trigger that opens the authenticated 渐进式UI popup (the user's OS login \ @@ -5069,8 +5072,10 @@ mod tests { 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); - assert!(out.contains("Ran"), "action verb for bash"); - assert!(out.contains("npm test"), "shows the command argument"); + // Bash args are token-colored (program/flags/args), so check visible text. + 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] diff --git a/src/tui/render.rs b/src/tui/render.rs index 1e97734..6921eaf 100644 --- a/src/tui/render.rs +++ b/src/tui/render.rs @@ -40,6 +40,13 @@ pub(crate) fn render_tool_end( .bold() .render("•"); let arg = args.and_then(arg_summary).unwrap_or_default(); + // Bash commands get Codex-style token coloring (program vs flags vs args); + // everything else keeps the muted single-color summary. + let arg_styled = if matches!(name, "bash" | "shell" | "run" | "exec") { + highlight_shell(&arg) + } else { + Style::new().fg(TN_GRAY).render(&arg) + }; let header = if arg.is_empty() { format!( "{margin}{dot} {}", @@ -49,7 +56,7 @@ pub(crate) fn render_tool_end( format!( "{margin}{dot} {} {}", Style::new().bold().render(tool_verb(name)), - Style::new().fg(TN_GRAY).render(&arg) + arg_styled ) }; @@ -291,6 +298,27 @@ pub(crate) fn tool_verb(name: &str) -> &str { /// Claude-Code-style tool label: `Tool(arg)`, e.g. "Bash(npm test)", /// "Read(src/main.rs)", "Update(lib.rs)". Used for the live-running indicator /// and the approval prompt. +/// Codex-style coloring for a shell command in a tool header: the program name +/// stands out (bold cyan), flags are distinct (yellow), and positional args are +/// muted (gray) so the line is scannable at a glance. +pub(crate) fn highlight_shell(cmd: &str) -> String { + let mut out = String::new(); + for (i, tok) in cmd.split_whitespace().enumerate() { + if i > 0 { + out.push(' '); + } + let styled = if i == 0 { + Style::new().fg(TN_CYAN).bold().render(tok) + } else if tok.starts_with('-') { + Style::new().fg(TN_YELLOW).render(tok) + } else { + Style::new().fg(TN_GRAY).render(tok) + }; + out.push_str(&styled); + } + out +} + pub(crate) fn tool_label(name: &str, args: Option<&serde_json::Value>) -> String { let target = args.and_then(arg_summary).unwrap_or_default(); let display = match name { @@ -483,3 +511,18 @@ pub(crate) fn render_diff(path: &str, before: &str, after: &str, width: usize) - } out } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn highlight_shell_colors_tokens_and_preserves_text() { + let s = highlight_shell("curl -s -X POST http://x"); + // Styling was applied (escape sequences present)... + assert!(s.contains('\u{1b}')); + // ...but the visible text is unchanged (single-spaced tokens). + assert_eq!(a3s_tui::style::strip_ansi(&s), "curl -s -X POST http://x"); + assert_eq!(highlight_shell(""), ""); + } +}