From 1a5721beb5ed029f8e763e622087e346eb3b2472 Mon Sep 17 00:00:00 2001 From: LHagfoss Date: Sun, 1 Nov 2026 16:59:28 +0100 Subject: [PATCH] feat(macos): turn Smart UI on by default and ask for visuals unasked Smart UI shipped opt-in and agents almost never reached it. It is now on by default, and the prompt asks for rustcode-viz charts for trends, comparisons, breakdowns and hierarchies, and for Smart UI when controls help, even when the user did not ask. Claude-Session: https://claude.ai/code/session_01KeJmpmSx6xtTEQSZMEJUBg --- docs/smart-ui.md | 12 +++++++----- docs/visualizations.md | 2 +- macos/Sources/RustCode/SettingsView.swift | 6 +++--- macos/Sources/RustCode/SmartUIView.swift | 2 +- macos/Sources/RustCodeKit/SmartUI.swift | 10 +++++++--- macos/Tests/RustCodeKitTests/SmartUITests.swift | 11 +++++++++++ rustcode/engine/src/skills_builtin/smart-ui.md | 2 +- rustcode/engine/src/tools/schema.rs | 6 +++--- 8 files changed, 34 insertions(+), 17 deletions(-) diff --git a/docs/smart-ui.md b/docs/smart-ui.md index 3f55e3b10..c196c2862 100644 --- a/docs/smart-ui.md +++ b/docs/smart-ui.md @@ -3,8 +3,8 @@ Smart UI ([#2484](https://github.com/LHagfoss/rustcode/issues/2484)) lets the agent answer with a small interactive component, such as a filterable table or a view driven by a few controls, that the macOS app runs in the -transcript. It is **experimental and off by default**: Settings › General › -Chat › Smart UI. The engine learns the setting when it starts, so after +transcript. It is **on by default** (it shipped opt-in, and agents almost +never reached it): Settings › General › Chat › Smart UI turns it off. The engine learns the setting when it starts, so after switching it Settings offers **Restart Engine** (shown once no chat is running). @@ -151,7 +151,9 @@ clicks do not reach WebKit. ## Recommendation -**Ship it limited**: opt-in, experimental, a single live view, with the +It shipped limited: opt-in, experimental, a single live view, with the controls above. The cost is bounded and paid only when a block runs, and the -isolation held in every check. Keep it off by default until it has been used -for a while with real pointer input and longer sessions. +isolation held in every check. Opt-in meant agents almost never used it, so +it is now on by default, and the prompt asks for visuals even unasked. +T3 Code saw the same thing: its rule "render a visual only when asked" took +inline charts from 8 of 8 eval cases to 0 of 8 (pingdotgg/t3code#25766). diff --git a/docs/visualizations.md b/docs/visualizations.md index c875a84ac..d21f878c0 100644 --- a/docs/visualizations.md +++ b/docs/visualizations.md @@ -41,7 +41,7 @@ Shared fields: A typed content block or a tool-result artifact would need protocol changes in every provider adapter and client. Interactive HTML uses the same fence -approach instead: the macOS app's opt-in [Smart UI](smart-ui.md) runs +approach instead: the macOS app's [Smart UI](smart-ui.md) runs `rustcode-ui v1` blocks in a sandboxed web view. ## Widget allowlist (version 1) diff --git a/macos/Sources/RustCode/SettingsView.swift b/macos/Sources/RustCode/SettingsView.swift index 0a3d904f9..6b94d60a2 100644 --- a/macos/Sources/RustCode/SettingsView.swift +++ b/macos/Sources/RustCode/SettingsView.swift @@ -474,7 +474,7 @@ private struct GeneralSettings: View { @AppStorage(StreamingProse.revealKey) private var revealsText = true @AppStorage(ToolVerbosity.key) private var verbosity = ToolVerbosity.terminalDefault @AppStorage(ComposerPredictions.key) private var predictions = true - @AppStorage(SmartUI.enabledKey) private var smartUI = false + @AppStorage(SmartUI.enabledKey) private var smartUI = SmartUI.enabledByDefault @AppStorage(Orchestration.enabledKey) private var threadMessaging = false @AppStorage(DesktopModel.groupsByAttentionKey) private var groupsByAttention = true @AppStorage(TurnCompletionCard.enabledKey) private var showsTurnCompletion = true @@ -566,9 +566,9 @@ private struct GeneralSettings: View { .toggleStyle(.switch) } RowDivider() - SettingsRow(title: "Smart UI (experimental)", + SettingsRow(title: "Smart UI", subtitle: smartUI == model.engineSmartUI - ? "Let the agent reply with small interactive views when you ask for one. They run sandboxed, with no network, file or app access" + ? "Let the agent reply with small interactive views when they help. They run sandboxed, with no network, file or app access" : "The agent learns this when the engine restarts; threads in progress keep running until then") { HStack(spacing: 8) { // The engine reads the setting at initialize. diff --git a/macos/Sources/RustCode/SmartUIView.swift b/macos/Sources/RustCode/SmartUIView.swift index fc54d5aa5..491b6ea06 100644 --- a/macos/Sources/RustCode/SmartUIView.swift +++ b/macos/Sources/RustCode/SmartUIView.swift @@ -13,7 +13,7 @@ struct SmartUIView: View { /// The reply is still arriving, so an open fence may yet close. var isStreaming = false @ObservedObject private var host = SmartUIHost.shared - @AppStorage(SmartUI.enabledKey) private var isEnabled = false + @AppStorage(SmartUI.enabledKey) private var isEnabled = SmartUI.enabledByDefault /// The block was written while on screen, so it runs once complete. @State private var wasBuilding = false @State private var showsSource = false diff --git a/macos/Sources/RustCodeKit/SmartUI.swift b/macos/Sources/RustCodeKit/SmartUI.swift index c1302ae9b..d5e026410 100644 --- a/macos/Sources/RustCodeKit/SmartUI.swift +++ b/macos/Sources/RustCodeKit/SmartUI.swift @@ -10,10 +10,14 @@ public enum SmartUI { public static let version = 1 public static var infoString: String { "\(infoName) v\(version)" } - /// The setting that turns Smart UI on. Off by default; the engine is told - /// at `initialize`, so a change applies once the engine restarts. + /// The setting that turns Smart UI on. On by default (#2484 shipped it + /// opt-in; agents rarely used it); the engine is told at `initialize`, + /// so a change applies once the engine restarts. public static let enabledKey = "smartUIEnabled" - public static var isEnabled: Bool { UserDefaults.standard.bool(forKey: enabledKey) } + public static let enabledByDefault = true + public static var isEnabled: Bool { + UserDefaults.standard.object(forKey: enabledKey) as? Bool ?? enabledByDefault + } /// The `_meta.rustcode.smartUi` version advertised while it is on. public static let capabilityVersion = 1 diff --git a/macos/Tests/RustCodeKitTests/SmartUITests.swift b/macos/Tests/RustCodeKitTests/SmartUITests.swift index d52f351e2..e2028f28d 100644 --- a/macos/Tests/RustCodeKitTests/SmartUITests.swift +++ b/macos/Tests/RustCodeKitTests/SmartUITests.swift @@ -78,4 +78,15 @@ final class SmartUITests: XCTestCase { XCTAssertEqual(on["clientCapabilities"]?["_meta"]?["rustcode"]?["visualizations"], .number(Double(Visualization.capabilityLevel))) } + func testSmartUIIsOnUntilTheUserTurnsItOff() { + let defaults = UserDefaults.standard + let was = defaults.object(forKey: SmartUI.enabledKey) + defer { defaults.set(was, forKey: SmartUI.enabledKey) } + defaults.removeObject(forKey: SmartUI.enabledKey) + XCTAssertTrue(SmartUI.isEnabled) + defaults.set(false, forKey: SmartUI.enabledKey) + XCTAssertFalse(SmartUI.isEnabled) + defaults.set(true, forKey: SmartUI.enabledKey) + XCTAssertTrue(SmartUI.isEnabled) + } } diff --git a/rustcode/engine/src/skills_builtin/smart-ui.md b/rustcode/engine/src/skills_builtin/smart-ui.md index 0d2b64c53..f5c176f86 100644 --- a/rustcode/engine/src/skills_builtin/smart-ui.md +++ b/rustcode/engine/src/skills_builtin/smart-ui.md @@ -1,6 +1,6 @@ --- name: smart-ui -description: Write a Smart UI (rustcode-ui v1) interactive component for the RustCode macOS chat. Use only when the prompt's Smart UI section says it is on and the user asks for an interactive view, or sorting, filtering, sliders or toggles clearly help. +description: Write a Smart UI (rustcode-ui v1) interactive component for the RustCode macOS chat. Use when the prompt's Smart UI section says it is on and the user asks for an interactive view, or sorting, filtering, sliders or toggles would help them explore the answer, even unasked. triggers: smart ui, interactive view, rustcode-ui keywords: interactive, widget, component, html, slider, filter, sort, toggle --- diff --git a/rustcode/engine/src/tools/schema.rs b/rustcode/engine/src/tools/schema.rs index 4a3610187..625863947 100644 --- a/rustcode/engine/src/tools/schema.rs +++ b/rustcode/engine/src/tools/schema.rs @@ -2462,7 +2462,7 @@ One line, no headings or lists, and never one per call. Skip it only when the ba /// Short guidance for `rustcode-viz` blocks; the full format lives in /// `docs/visualizations.md` and the TUI renderer validates every payload. pub(crate) const VISUALIZATION_PROMPT_HINT: &str = "# Visualizations\n\ -When a chart, table, or tree beats prose, add a ```rustcode-viz fence with one JSON object: \ +When a chart, table, or tree beats prose (trends, comparisons, breakdowns, hierarchies), add a ```rustcode-viz fence even if not asked with one JSON object: \ {\"version\":1,\"type\":\"bar_chart\",\"title\":\"..\",\"summary\":\"..\",\"items\":[{\"label\":\"..\",\"value\":1}]}; \ \"table\" takes columns, rows (arrays), optional sort_by {column,descending}; \"tree\" takes nodes [{label,note,children}]. Max 30 bars, 50 rows, 120 nodes.\n\n"; @@ -2503,8 +2503,8 @@ pub(crate) fn append_visualization_hint( /// advertise `_meta.rustcode.smartUi`; the format lives in `docs/smart-ui.md`. pub(crate) const SMART_UI_PROMPT_HINT: &str = "# Smart UI\n\ This app can run one small interactive component in a reply, written as a ```rustcode-ui v1 fence of self-contained HTML (no network). \ -Use it only when the user asks for an interactive view or controls such as sorting, filtering or sliders clearly help; \ -otherwise use Markdown or rustcode-viz. Before writing one, load the `smart-ui` skill for the format and limits.\n\n"; +Use it, even unasked, when controls such as sorting, filtering, sliders or toggles let the reader explore what a static chart cannot show; \ +for a plain chart, table or tree use rustcode-viz. Before writing one, load the `smart-ui` skill for the format and limits.\n\n"; /// Sent instead of [`SMART_UI_PROMPT_HINT`] when the frontend could run the /// blocks but the user has the setting off. Without it an agent asked for