Skip to content

Commit 95e2d47

Browse files
committed
feat(diagrams): add provider and model tracking for diagram creation and rendering
1 parent 2df6a49 commit 95e2d47

9 files changed

Lines changed: 168 additions & 46 deletions

File tree

src-tauri/src/agent/mermaid/commands.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ pub fn mermaid_create_diagram(
3939
kind.as_deref().unwrap_or(""),
4040
task_id,
4141
id,
42+
// UI-triggered manual creation: no generating model to record.
43+
None,
44+
None,
4245
)
4346
}
4447

src-tauri/src/agent/mermaid/store.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ pub struct DiagramMeta {
4141
pub task_id: Option<String>,
4242
/// Creation time, ms since UNIX epoch.
4343
pub created_ms: u64,
44+
/// Provider that generated the diagram (e.g. `openrouter`), as
45+
/// `AgentProviderKind::as_str`. Absent for diagrams created before this was
46+
/// recorded.
47+
#[serde(default, skip_serializing_if = "Option::is_none")]
48+
pub provider: Option<String>,
49+
/// Model id that generated the diagram (e.g. `openai/gpt-5`). Absent for
50+
/// diagrams created before this was recorded.
51+
#[serde(default, skip_serializing_if = "Option::is_none")]
52+
pub model: Option<String>,
4453
}
4554

4655
/// A diagram with its source code loaded. Returned to the frontend gallery.
@@ -135,6 +144,7 @@ fn slugify_unique(title: &str, existing: &[DiagramMeta]) -> String {
135144

136145
/// Persist a new diagram under the given plan. Returns the stored record
137146
/// (with a freshly-allocated id when `id` is `None`).
147+
#[allow(clippy::too_many_arguments)]
138148
pub fn create_diagram(
139149
ws: &str,
140150
slug: &str,
@@ -143,6 +153,8 @@ pub fn create_diagram(
143153
kind: &str,
144154
task_id: Option<String>,
145155
id: Option<String>,
156+
provider: Option<String>,
157+
model: Option<String>,
146158
) -> Result<DiagramRecord, String> {
147159
let dir = diagrams_dir(ws, slug)?;
148160
let mut manifest = read_manifest(&dir);
@@ -163,6 +175,8 @@ pub fn create_diagram(
163175
kind: kind.trim().to_string(),
164176
task_id: task_id.filter(|t| !t.trim().is_empty()),
165177
created_ms: now_ms(),
178+
provider: provider.filter(|p| !p.trim().is_empty()),
179+
model: model.filter(|m| !m.trim().is_empty()),
166180
};
167181

168182
fs::create_dir_all(&dir).map_err(|e| format!("create diagrams dir: {e}"))?;
@@ -230,10 +244,14 @@ mod tests {
230244
"flowchart",
231245
Some("setup-auth".into()),
232246
None,
247+
Some("openrouter".into()),
248+
Some("openai/gpt-5".into()),
233249
)
234250
.unwrap();
235251
assert_eq!(rec.meta.id, "auth-flow");
236252
assert_eq!(rec.meta.task_id.as_deref(), Some("setup-auth"));
253+
assert_eq!(rec.meta.provider.as_deref(), Some("openrouter"));
254+
assert_eq!(rec.meta.model.as_deref(), Some("openai/gpt-5"));
237255

238256
let listed = list_diagrams(&ws, "my-plan").unwrap();
239257
assert_eq!(listed.len(), 1);
@@ -246,15 +264,18 @@ mod tests {
246264
#[test]
247265
fn id_collisions_get_suffixed() {
248266
let ws = tmp_ws();
249-
let a = create_diagram(&ws, "p", "Flow", "a", "flowchart", None, None).unwrap();
250-
let b = create_diagram(&ws, "p", "Flow", "b", "flowchart", None, None).unwrap();
267+
let a = create_diagram(&ws, "p", "Flow", "a", "flowchart", None, None, None, None).unwrap();
268+
let b = create_diagram(&ws, "p", "Flow", "b", "flowchart", None, None, None, None).unwrap();
251269
assert_eq!(a.meta.id, "flow");
252270
assert_eq!(b.meta.id, "flow-2");
253271
}
254272

255273
#[test]
256274
fn rejects_path_traversal_id() {
257275
let ws = tmp_ws();
258-
assert!(create_diagram(&ws, "p", "x", "c", "k", None, Some("../evil".into())).is_err());
276+
assert!(
277+
create_diagram(&ws, "p", "x", "c", "k", None, Some("../evil".into()), None, None)
278+
.is_err()
279+
);
259280
}
260281
}

src-tauri/src/agent/mermaid/tool.rs

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ struct DiagramOut {
3131
task_id: Option<String>,
3232
plan_slug: Option<String>,
3333
persisted: bool,
34+
/// Provider/model that generated this diagram, when known. Stamped for both
35+
/// persisted and ephemeral diagrams so the gallery can show it.
36+
#[serde(skip_serializing_if = "Option::is_none")]
37+
provider: Option<String>,
38+
#[serde(skip_serializing_if = "Option::is_none")]
39+
model: Option<String>,
40+
}
41+
42+
/// Provider/model that issued a `mermaid_create*` call, threaded from the
43+
/// dispatch context so each diagram records what generated it.
44+
#[derive(Clone, Default)]
45+
pub struct DiagramOrigin {
46+
pub provider: Option<String>,
47+
pub model: Option<String>,
3448
}
3549

3650
#[derive(Serialize)]
@@ -40,13 +54,13 @@ struct DiagramsEnvelope {
4054

4155
/// Result of building/persisting a set of diagrams: a JSON string ready for the
4256
/// tool `content`, or an error message.
43-
pub fn run_create(ws: &str, args: &Value) -> Result<String, String> {
57+
pub fn run_create(ws: &str, args: &Value, origin: &DiagramOrigin) -> Result<String, String> {
4458
let one = parse_one(args)?;
45-
let out = build_one(ws, one)?;
59+
let out = build_one(ws, one, origin)?;
4660
finish(vec![out])
4761
}
4862

49-
pub fn run_create_many(ws: &str, args: &Value) -> Result<String, String> {
63+
pub fn run_create_many(ws: &str, args: &Value, origin: &DiagramOrigin) -> Result<String, String> {
5064
let plan_slug = opt_str(args, "plan_slug");
5165
let items = args
5266
.get("diagrams")
@@ -65,7 +79,7 @@ pub fn run_create_many(ws: &str, args: &Value) -> Result<String, String> {
6579
if spec.plan_slug.is_none() {
6680
spec.plan_slug = plan_slug.clone();
6781
}
68-
out.push(build_one(ws, spec)?);
82+
out.push(build_one(ws, spec, origin)?);
6983
}
7084
finish(out)
7185
}
@@ -98,7 +112,7 @@ fn parse_one(v: &Value) -> Result<Spec, String> {
98112
})
99113
}
100114

101-
fn build_one(ws: &str, spec: Spec) -> Result<DiagramOut, String> {
115+
fn build_one(ws: &str, spec: Spec, origin: &DiagramOrigin) -> Result<DiagramOut, String> {
102116
match &spec.plan_slug {
103117
Some(slug) => {
104118
let rec = store::create_diagram(
@@ -109,6 +123,8 @@ fn build_one(ws: &str, spec: Spec) -> Result<DiagramOut, String> {
109123
&spec.kind,
110124
spec.task_id.clone(),
111125
spec.id,
126+
origin.provider.clone(),
127+
origin.model.clone(),
112128
)?;
113129
Ok(DiagramOut {
114130
id: rec.meta.id,
@@ -118,6 +134,8 @@ fn build_one(ws: &str, spec: Spec) -> Result<DiagramOut, String> {
118134
task_id: rec.meta.task_id,
119135
plan_slug: Some(slug.clone()),
120136
persisted: true,
137+
provider: rec.meta.provider,
138+
model: rec.meta.model,
121139
})
122140
}
123141
None => Ok(DiagramOut {
@@ -128,6 +146,8 @@ fn build_one(ws: &str, spec: Spec) -> Result<DiagramOut, String> {
128146
task_id: spec.task_id,
129147
plan_slug: None,
130148
persisted: false,
149+
provider: origin.provider.clone(),
150+
model: origin.model.clone(),
131151
}),
132152
}
133153
}
@@ -174,25 +194,39 @@ mod tests {
174194

175195
#[test]
176196
fn ephemeral_when_no_plan_slug() {
197+
let origin = DiagramOrigin {
198+
provider: Some("openrouter".into()),
199+
model: Some("openai/gpt-5".into()),
200+
};
177201
let out = run_create(
178202
"/nonexistent-ws-ignored",
179203
&json!({ "title": "Flow", "code": "flowchart TD\n A-->B" }),
204+
&origin,
180205
)
181206
.unwrap();
182207
assert!(out.contains("\"persisted\":false"));
183208
assert!(out.contains("\"id\":\"flow\""));
209+
// Origin is stamped onto ephemeral diagrams too.
210+
assert!(out.contains("\"provider\":\"openrouter\""));
211+
assert!(out.contains("\"model\":\"openai/gpt-5\""));
184212
}
185213

186214
#[test]
187215
fn rejects_oversize_code() {
188216
let big = "x".repeat(MAX_CODE_BYTES + 1);
189-
let err = run_create("/ws", &json!({ "title": "T", "code": big })).unwrap_err();
217+
let err = run_create(
218+
"/ws",
219+
&json!({ "title": "T", "code": big }),
220+
&DiagramOrigin::default(),
221+
)
222+
.unwrap_err();
190223
assert!(err.contains("byte limit"));
191224
}
192225

193226
#[test]
194227
fn many_rejects_empty() {
195-
let err = run_create_many("/ws", &json!({ "diagrams": [] })).unwrap_err();
228+
let err = run_create_many("/ws", &json!({ "diagrams": [] }), &DiagramOrigin::default())
229+
.unwrap_err();
196230
assert!(err.contains("empty"));
197231
}
198232
}

src-tauri/src/agent/subagent_runner.rs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ pub async fn run_one_subagent(
357357
&args,
358358
groups,
359359
root_guard.as_ref(),
360+
ctx,
360361
);
361362
let tool_elapsed_ms =
362363
tool_start.elapsed().as_millis().min(u64::MAX as u128) as u64;
@@ -501,8 +502,13 @@ pub async fn run_one_subagent(
501502
ToolCallOutcome::NotSubmit => {
502503
let args: Value = serde_json::from_str(&args_str).unwrap_or(json!({}));
503504
let tool_start = Instant::now();
504-
let outcome =
505-
execute_subagent_tool(&name, &args, groups, root_guard.as_ref());
505+
let outcome = execute_subagent_tool(
506+
&name,
507+
&args,
508+
groups,
509+
root_guard.as_ref(),
510+
ctx,
511+
);
506512
let tool_elapsed_ms =
507513
tool_start.elapsed().as_millis().min(u64::MAX as u128) as u64;
508514
state.push(AgentEvent::TurnUsage {
@@ -615,20 +621,20 @@ fn execute_subagent_tool(
615621
args: &Value,
616622
groups: &[ToolGroup],
617623
root: Option<&WorkspaceRootGuard>,
624+
ctx: &DispatchContext,
618625
) -> tools::ToolOutcome {
619-
let shell_write = groups.contains(&ToolGroup::ShellWrite);
620-
if name == "shell_exec" {
621-
tools::execute_server_tool(
622-
name,
623-
args,
624-
root,
625-
Some(tools::ToolExecOpts {
626-
shell_writes: shell_write,
627-
}),
628-
)
629-
} else {
630-
tools::execute_server_tool(name, args, root, None)
631-
}
626+
let shell_write = name == "shell_exec" && groups.contains(&ToolGroup::ShellWrite);
627+
// Stamp the subagent's own provider/model so diagrams it creates record it.
628+
tools::execute_server_tool(
629+
name,
630+
args,
631+
root,
632+
Some(tools::ToolExecOpts {
633+
shell_writes: shell_write,
634+
provider: Some(ctx.settings.provider.as_str().to_string()),
635+
model: Some(ctx.settings.model_id.clone()),
636+
}),
637+
)
632638
}
633639

634640
fn finish_subagent(state: &Arc<AgentEngineState>, agent_id: &str, result: &Value) {

src-tauri/src/agent/tool_dispatch.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub async fn dispatch_tool(
5151
},
5252
}
5353
} else {
54-
dispatch_regular_tool(state, call_id, name, args, root).await
54+
dispatch_regular_tool(state, call_id, name, args, root, ctx).await
5555
};
5656
state.pop_parent();
5757
outcome
@@ -270,6 +270,7 @@ async fn dispatch_regular_tool(
270270
name: &str,
271271
args: &Value,
272272
root: Option<&WorkspaceRootGuard>,
273+
ctx: Option<&DispatchContext>,
273274
) -> tools::ToolOutcome {
274275
// MCP-server tools are not in the static registry; route them to the live
275276
// client runtime before the normal lookup.
@@ -291,7 +292,16 @@ async fn dispatch_regular_tool(
291292
};
292293

293294
match def.site {
294-
ToolSite::Server => tools::execute_server_tool(name, args, root, None),
295+
ToolSite::Server => {
296+
// Stamp the active provider/model so created diagrams record what
297+
// generated them.
298+
let opts = ctx.map(|c| tools::ToolExecOpts {
299+
shell_writes: false,
300+
provider: Some(c.settings.provider.as_str().to_string()),
301+
model: Some(c.settings.model_id.clone()),
302+
});
303+
tools::execute_server_tool(name, args, root, opts)
304+
}
295305
ToolSite::Client => wait_for_client_tool(state, call_id, name).await,
296306
}
297307
}

src-tauri/src/agent/tools.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,13 @@ pub struct ToolDef {
233233
pub site: ToolSite,
234234
}
235235

236-
/// Options for server-tool execution (shell write mode, etc.).
237-
#[derive(Clone, Copy, Debug, Default)]
236+
/// Options for server-tool execution (shell write mode, generating model, etc.).
237+
#[derive(Clone, Debug, Default)]
238238
pub struct ToolExecOpts {
239239
pub shell_writes: bool,
240+
/// Provider/model that issued the call, stamped onto created diagrams.
241+
pub provider: Option<String>,
242+
pub model: Option<String>,
240243
}
241244

242245
/// Output of an in-process server-tool execution.
@@ -1650,7 +1653,12 @@ pub fn execute_server_tool(
16501653
root: Option<&WorkspaceRootGuard>,
16511654
opts: Option<ToolExecOpts>,
16521655
) -> ToolOutcome {
1653-
let shell_writes = opts.map(|o| o.shell_writes).unwrap_or(false);
1656+
let opts = opts.unwrap_or_default();
1657+
let shell_writes = opts.shell_writes;
1658+
let origin = crate::agent::mermaid::tool::DiagramOrigin {
1659+
provider: opts.provider.clone(),
1660+
model: opts.model.clone(),
1661+
};
16541662
match name {
16551663
"environment_detect" => crate::agent::environment::tool_environment_detect(root),
16561664
"shell_exec" => crate::agent::shell_exec::tool_shell_exec(args, root, shell_writes),
@@ -1701,8 +1709,8 @@ pub fn execute_server_tool(
17011709
"plan_rename" => tool_plan_rename(args, root),
17021710
"plan_load" => tool_plan_load(args, root),
17031711
"plan_sync_from_tasks" => tool_plan_sync_from_tasks(args, root),
1704-
"mermaid_create" => tool_mermaid_create(args, root),
1705-
"mermaid_create_many" => tool_mermaid_create_many(args, root),
1712+
"mermaid_create" => tool_mermaid_create(args, root, &origin),
1713+
"mermaid_create_many" => tool_mermaid_create_many(args, root, &origin),
17061714
"kanban_board_load" => tool_kanban_board_load(root),
17071715
"kanban_layout_save" => tool_kanban_layout_save(args, root),
17081716
"kanban_task_create" => tool_kanban_task_create(args, root),
@@ -2901,23 +2909,31 @@ fn tool_plan_create(args: &Value, root: Option<&WorkspaceRootGuard>) -> ToolOutc
29012909
}
29022910
}
29032911

2904-
fn tool_mermaid_create(args: &Value, root: Option<&WorkspaceRootGuard>) -> ToolOutcome {
2912+
fn tool_mermaid_create(
2913+
args: &Value,
2914+
root: Option<&WorkspaceRootGuard>,
2915+
origin: &crate::agent::mermaid::tool::DiagramOrigin,
2916+
) -> ToolOutcome {
29052917
let ws = match workspace_string(root) {
29062918
Ok(s) => s,
29072919
Err(out) => return out,
29082920
};
2909-
match crate::agent::mermaid::tool::run_create(&ws, args) {
2921+
match crate::agent::mermaid::tool::run_create(&ws, args, origin) {
29102922
Ok(content) => ToolOutcome { ok: true, content },
29112923
Err(e) => err_outcome(e),
29122924
}
29132925
}
29142926

2915-
fn tool_mermaid_create_many(args: &Value, root: Option<&WorkspaceRootGuard>) -> ToolOutcome {
2927+
fn tool_mermaid_create_many(
2928+
args: &Value,
2929+
root: Option<&WorkspaceRootGuard>,
2930+
origin: &crate::agent::mermaid::tool::DiagramOrigin,
2931+
) -> ToolOutcome {
29162932
let ws = match workspace_string(root) {
29172933
Ok(s) => s,
29182934
Err(out) => return out,
29192935
};
2920-
match crate::agent::mermaid::tool::run_create_many(&ws, args) {
2936+
match crate::agent::mermaid::tool::run_create_many(&ws, args, origin) {
29212937
Ok(content) => ToolOutcome { ok: true, content },
29222938
Err(e) => err_outcome(e),
29232939
}

src-tauri/src/plans.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,8 @@ mod tests {
13031303
"flowchart",
13041304
None,
13051305
None,
1306+
None,
1307+
None,
13061308
)
13071309
.unwrap();
13081310

0 commit comments

Comments
 (0)