Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6c93a65
Send compact tool display payloads and fetch full results on demand
juacker Sep 27, 2026
3d8bdf5
Persist failed tool payload before announcing completion
juacker Sep 27, 2026
68d2968
Cover persisted tool-message result lookup
juacker Sep 27, 2026
19159c6
Fetch full tool input and output lazily when a chat row expands
juacker Sep 27, 2026
d15fdf8
Compact chat tool inputs and load full input on demand
juacker Sep 27, 2026
5bf034a
Set full input flag only when tool params are compacted
juacker Sep 27, 2026
9214245
Drop unreachable tool-message fallback from result lookup
juacker Sep 27, 2026
7af72e3
Look up tool call input in the call's own session only
juacker Sep 27, 2026
44061bf
Keep a cached tool detail on an open row after cache eviction
juacker Sep 27, 2026
7b980d0
Announce tool completion before creating the tool message
juacker Sep 27, 2026
34d4e4f
Summarise writes and fetches only once a result exists
juacker Sep 27, 2026
ecf0ca3
Format tool call session lookup test
juacker Sep 27, 2026
9b9859e
Satisfy clippy slice lint in tool call lookup test
juacker Sep 27, 2026
7677c74
Replace a displayed create_vega_chart row with its chart card
juacker Sep 27, 2026
38e2695
Show a saved chart as a row that opens it in artifacts
juacker Sep 27, 2026
5c14cc9
Open a chat chart row's file in the artifacts drawer
juacker Sep 27, 2026
21d3ba9
Make the embedded markdown the only chat display path for create_vega…
juacker Sep 27, 2026
40a8177
Keep only ok and path in the compact create_vega_chart result
juacker Sep 27, 2026
83a361b
Read a chart row's path only from the tool result
juacker Sep 27, 2026
02e747a
Open a chart saved under memory as a memory, not an artifact
juacker Sep 27, 2026
1dc6d81
Give the chart row a neutral open label
juacker Sep 27, 2026
234b6d9
Name the chat's file opener for what it opens
juacker Sep 27, 2026
af7642c
Type the readWorkspaceFile mock as the real API
juacker Sep 27, 2026
220c01b
Show an explicit "Open chart" action on saved-chart rows
juacker Sep 27, 2026
593f5ec
Move a chart's Open action from its tool row to the embedded chart
juacker Sep 27, 2026
0e2219a
Drop the embedded chart's header and Open action
juacker Sep 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
503 changes: 503 additions & 0 deletions src-tauri/src/assistant/display.rs

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src-tauri/src/assistant/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
use ts_rs::TS;

use crate::assistant::display;
use crate::assistant::tools::ask_user::AskUserOption;
use crate::assistant::types::{
AssistantCompaction, AssistantMessage, AssistantRun, AssistantSession, MessageId, RunId,
Expand Down Expand Up @@ -135,7 +136,7 @@ pub fn emit_event(
run_id: run_id.map(str::to_string),
workspace_id: session.context.workspace_id.clone(),
timestamp: Utc::now().timestamp_millis(),
event,
event: display::event(event),
};

app.emit(ASSISTANT_EVENT_NAME, envelope)
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/assistant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod codex_app_server;
pub mod compaction;
pub mod compaction_service;
pub mod conversation;
pub mod display;
pub mod engine;
pub mod events;
pub mod image_store;
Expand Down
22 changes: 22 additions & 0 deletions src-tauri/src/assistant/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,9 @@ fn map_tool_call_row(row: &sqlx::sqlite::SqliteRow) -> Result<ToolInvocation, St
params: parse_json(&row.get::<String, _>("params_json"), "tool call params")?,
status: parse_json::<ToolCallStatus>(&row.get::<String, _>("status"), "tool call status")?,
result: parse_optional_json(row.get("result_json"), "tool call result")?,
result_summary: None,
has_full_result: false,
has_full_input: false,
error: row.get("error"),
started_at: row.get("started_at"),
completed_at: row.get("completed_at"),
Expand All @@ -1326,6 +1329,9 @@ pub async fn create_tool_call(
params: params.params,
status: params.status,
result: None,
result_summary: None,
has_full_result: false,
has_full_input: false,
error: None,
started_at: now_ms(),
completed_at: None,
Expand Down Expand Up @@ -1396,6 +1402,22 @@ pub async fn update_tool_call(
map_tool_call_row(&row)
}

pub async fn get_tool_call_for_session(
pool: &DbPool,
session_id: &str,
tool_call_id: &str,
) -> Result<Option<ToolInvocation>, String> {
let row = sqlx::query(
"SELECT id, run_id, session_id, tool_name, params_json, status, result_json, error, started_at, completed_at FROM assistant_tool_calls WHERE session_id = ? AND id = ?",
)
.bind(session_id)
.bind(tool_call_id)
.fetch_optional(pool)
.await
.map_err(|e| format!("Failed to load assistant tool call: {}", e))?;
row.as_ref().map(map_tool_call_row).transpose()
}

pub async fn list_tool_calls(
pool: &DbPool,
session_id: &str,
Expand Down
170 changes: 170 additions & 0 deletions src-tauri/src/assistant/repository_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1030,3 +1030,173 @@ async fn test_list_messages_orders_same_timestamp_by_insertion() {

assert_eq!(loaded, inserted);
}

#[tokio::test]
async fn tool_result_lookup_is_session_scoped_and_keeps_full_payload() {
let (_tmp, pool) = workspace_pool().await;
let first = create_session(
&pool,
CreateSessionParams {
kind: SessionKind::Interactive,
title: None,
context: sample_context(),
},
)
.await
.unwrap();
let second = create_session(
&pool,
CreateSessionParams {
kind: SessionKind::Interactive,
title: None,
context: sample_context(),
},
)
.await
.unwrap();
let run = create_run(
&pool,
CreateRunParams {
session_id: first.id.clone(),
status: RunStatus::Running,
trigger: RunTrigger::UserMessage,
connection_id: "conn".into(),
protocol_id: "openai".into(),
model_id: "model".into(),
error: None,
},
)
.await
.unwrap();
create_tool_call(
&pool,
CreateToolCallParams {
id: "call-1".into(),
run_id: run.id,
session_id: first.id.clone(),
tool_name: "bash_exec".into(),
params: serde_json::json!({}),
status: ToolCallStatus::Running,
},
)
.await
.unwrap();
let full = serde_json::json!({"stdout": "full output", "exitCode": 0});
update_tool_call(
&pool,
"call-1",
ToolCallStatus::Completed,
Some(&full),
None,
)
.await
.unwrap();

assert_eq!(
get_tool_call_for_session(&pool, &first.id, "call-1")
.await
.unwrap()
.unwrap()
.result,
Some(full)
);
assert!(get_tool_call_for_session(&pool, &second.id, "call-1")
.await
.unwrap()
.is_none());
}

#[tokio::test]
async fn tool_input_lookup_is_session_scoped_and_preserves_stored_copies() {
use super::display;

let (_tmp, pool) = workspace_pool().await;
let mut sessions = Vec::new();
for _ in 0..2 {
sessions.push(
create_session(
&pool,
CreateSessionParams {
kind: SessionKind::Interactive,
title: None,
context: sample_context(),
},
)
.await
.unwrap(),
);
}
let run = create_run(
&pool,
CreateRunParams {
session_id: sessions[0].id.clone(),
status: RunStatus::Running,
trigger: RunTrigger::UserMessage,
connection_id: "conn".into(),
protocol_id: "openai".into(),
model_id: "model".into(),
error: None,
},
)
.await
.unwrap();
let full = serde_json::json!({"path":"report.md", "content":"long content".repeat(10_000)});
let original = create_tool_call(
&pool,
CreateToolCallParams {
id: "large-input".into(),
run_id: run.id,
session_id: sessions[0].id.clone(),
tool_name: "fs_write".into(),
params: full.clone(),
status: ToolCallStatus::Completed,
},
)
.await
.unwrap();
create_message(
&pool,
CreateMessageParams {
session_id: sessions[0].id.clone(),
role: MessageRole::Assistant,
content: vec![ContentPart::ToolUse {
tool_call_id: original.id.clone(),
tool_name: original.tool_name.clone(),
arguments: full.clone(),
}],
provider_metadata: None,
},
)
.await
.unwrap();
let page = display::page(AssistantMessagePage {
messages: list_messages(&pool, &sessions[0].id).await.unwrap(),
tool_calls: list_tool_calls_by_ids(&pool, std::slice::from_ref(&original.id))
.await
.unwrap(),
next_cursor: None,
has_more: false,
total_count: 1,
});
let page_json = serde_json::to_string(&page).unwrap();
assert!(!page_json.contains("long content"));
assert!(page_json.contains("report.md"));
assert_eq!(
get_tool_call_for_session(&pool, &sessions[0].id, &original.id)
.await
.unwrap()
.unwrap()
.params,
full
);
assert!(
get_tool_call_for_session(&pool, &sessions[1].id, &original.id)
.await
.unwrap()
.is_none()
);
let stored = list_messages(&pool, &sessions[0].id).await.unwrap();
assert!(
matches!(&stored[0].content[0], ContentPart::ToolUse { arguments, .. } if arguments == &full)
);
}
3 changes: 3 additions & 0 deletions src-tauri/src/assistant/run_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@ mod tests {
params: serde_json::json!({}),
status,
result: None,
result_summary: None,
has_full_result: false,
has_full_input: false,
error: None,
started_at: 0,
completed_at: None,
Expand Down
5 changes: 4 additions & 1 deletion src-tauri/src/assistant/system_prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ pub(crate) fn build_system_prompt(
"- Choose a chart proactively when it makes a pattern, comparison, distribution, trend, correlation, composition, or relationship materially easier to understand than prose or a short table. Skip charts for a single fact, a one-step action, a short list, or data with no meaningful visual structure.\n",
);
prompt.push_str(
"- Charts: create every chart with the `create_vega_chart` tool, one chart per call — never hand-write a Vega-Lite spec into a file or paste one into chat. The tool validates the spec (fix and retry on a schema error). Give it the complete workspace-relative `.vl.json` path and choose a visible artifact location that keeps the chart with its related task or document instead of defaulting to a shared charts directory (for example, `reports/q3/revenue.vl.json`); avoid cache, dependency, and build-output directories. Reuse an existing chart's exact path when updating it instead of creating a near-duplicate. The saved chart renders inline in the chat and as an artifact, and you embed it in a markdown document as `![title](/reports/q3/revenue.vl.json)` — leading `/` = workspace root, so the link works from a report in any folder (the tool returns that snippet as `markdown`).\n",
"- Charts: create every chart with the `create_vega_chart` tool, one chart per call — never hand-write a Vega-Lite spec into a file or paste one into chat. The tool validates the spec (fix and retry on a schema error). Give it the complete workspace-relative `.vl.json` path and choose a visible artifact location that keeps the chart with its related task or document instead of defaulting to a shared charts directory (for example, `reports/q3/revenue.vl.json`); avoid cache, dependency, and build-output directories. Reuse an existing chart's exact path when updating it instead of creating a near-duplicate. The tool does not show the chart in chat: it returns a `markdown` snippet such as `![title](/reports/q3/revenue.vl.json)`, and embedding that snippet in your reply or in a markdown report is the only way the chart is displayed — put it exactly where the chart belongs, once. Leading `/` = workspace root, so the link works from a report in any folder. Saved charts also appear as artifacts.\n",
);
if can_write_files {
prompt.push_str(
Expand Down Expand Up @@ -847,6 +847,9 @@ mod tests {
assert!(text.contains("avoid cache, dependency, and build-output directories"));
assert!(text.contains("Reuse an existing chart's exact path"));
assert!(text.contains("![title](/reports/q3/revenue.vl.json)"));
// Embedding the returned snippet is the only chat display path.
assert!(text.contains("the only way the chart is displayed"));
assert!(!text.contains("renders inline"));
assert!(!text.contains("charts/<slug>.vl.json"));
// Large tables go to a workspace file referenced by data.url.
assert!(text.contains("`data.url`"));
Expand Down
5 changes: 2 additions & 3 deletions src-tauri/src/assistant/tools/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,13 @@ fn history_query_def() -> ToolDefinition {
fn create_vega_chart_def() -> ToolDefinition {
ToolDefinition {
name: "create_vega_chart".to_string(),
description: "Create a chart. This is the ONLY way to produce a chart: never write a Vega-Lite spec directly with bash_exec or paste one into chat. The spec is validated against the Vega-Lite v6 JSON schema; on failure the error lists the violations with their JSON paths so you can fix the spec and call again. Provide the complete workspace-relative `.vl.json` `path` on every call: with `spec`, the validated chart is saved there; without `spec`, the existing chart there is validated. Choose a path that keeps the chart with its related task or document instead of defaulting to a shared charts directory; avoid cache, dependency, and build-output directories hidden from workspace artifacts. Reuse an existing chart's exact path when updating it instead of creating a near-duplicate. The tool returns `{ok, path, markdown}`. The saved file renders as an interactive chart in the chat (when `display` is true), in the artifacts panel when stored in a visible artifact directory, and inside markdown documents via `![title](/reports/q3/chart.vl.json)` (leading `/` = workspace root, so it works from a report in any folder) — paste the returned `markdown` into reports. One chart per call. Keep data out of the spec above ~50 rows: write it to a CSV/JSON file in the workspace with bash_exec and reference it with `data.url` (a leading `/` is workspace-root-relative, e.g. `/data/sales.csv`) instead of inlining `data.values`.".to_string(),
description: "Create a chart. This is the ONLY way to produce a chart: never write a Vega-Lite spec directly with bash_exec or paste one into chat. The spec is validated against the Vega-Lite v6 JSON schema; on failure the error lists the violations with their JSON paths so you can fix the spec and call again. Provide the complete workspace-relative `.vl.json` `path` on every call: with `spec`, the validated chart is saved there; without `spec`, the existing chart there is validated. Choose a path that keeps the chart with its related task or document instead of defaulting to a shared charts directory; avoid cache, dependency, and build-output directories hidden from workspace artifacts. Reuse an existing chart's exact path when updating it instead of creating a near-duplicate. The tool returns `{ok, path, markdown}`. The saved chart is NOT shown in chat by this call: to show it in your reply or in a markdown report, embed the returned `markdown` (`![title](/reports/q3/chart.vl.json)`; leading `/` = workspace root, so it works from a report in any folder) exactly where the chart belongs. It also appears in the artifacts panel when stored in a visible artifact directory. One chart per call. Keep data out of the spec above ~50 rows: write it to a CSV/JSON file in the workspace with bash_exec and reference it with `data.url` (a leading `/` is workspace-root-relative, e.g. `/data/sales.csv`) instead of inlining `data.values`.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"title": { "type": "string", "description": "Chart title used for the result and markdown alt text." },
"spec": { "type": ["object", "string"], "description": "A Vega-Lite v6 spec as a JSON object (a JSON string is also accepted). Omit `$schema` or set it to https://vega.github.io/schema/vega-lite/v6.json." },
"path": { "type": "string", "description": "Complete workspace-relative `.vl.json` path using ASCII letters, digits, `/`, `.`, `_`, and `-`. Choose a visible artifact location near related artifacts or documents, avoid cache/dependency/build-output directories, and reuse an existing chart's exact path when updating it. This is where `spec` is saved, or — when `spec` is omitted — the existing file to validate." },
"display": { "type": "boolean", "description": "Render the chart inline in the chat as this call's result (default true). Set false for charts that only belong in a report." }
"path": { "type": "string", "description": "Complete workspace-relative `.vl.json` path using ASCII letters, digits, `/`, `.`, `_`, and `-`. Choose a visible artifact location near related artifacts or documents, avoid cache/dependency/build-output directories, and reuse an existing chart's exact path when updating it. This is where `spec` is saved, or — when `spec` is omitted — the existing file to validate." }
},
"required": ["title", "path"],
"additionalProperties": false
Expand Down
Loading
Loading