diff --git a/docs/smart-ui.md b/docs/smart-ui.md index 7ee412fe7..6398f7763 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). @@ -179,7 +179,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 79a4e54cc..89d06aa3c 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 30caa6f47..bc8d9b2d5 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. /// Version 2 also answers `previewMethod`, so the engine offers the agent /// `smart_ui_preview` (`engine/src/smart_ui.rs`). diff --git a/macos/Tests/RustCodeKitTests/SmartUITests.swift b/macos/Tests/RustCodeKitTests/SmartUITests.swift index 6fb420fa0..98cd5fbd4 100644 --- a/macos/Tests/RustCodeKitTests/SmartUITests.swift +++ b/macos/Tests/RustCodeKitTests/SmartUITests.swift @@ -78,6 +78,19 @@ 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) + } + // MARK: - smart_ui_preview requests func testPreviewRequestReadsHtmlAndSessionAndTheCardTitle() throws { diff --git a/rustcode/engine/src/skills_builtin/smart-ui.md b/rustcode/engine/src/skills_builtin/smart-ui.md index 982728a72..bc07dbb13 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 bc4304b79..4d160ec13 100644 --- a/rustcode/engine/src/tools/schema.rs +++ b/rustcode/engine/src/tools/schema.rs @@ -2477,7 +2477,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"; @@ -2518,8 +2518,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