From f6b55b97eddc4279d7e9a733d154c5d9a8627f27 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 06:38:59 +0000 Subject: [PATCH 1/2] Show which skill invoked a subagent in the conversation view. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attribute each subagent to its invoking skill — primarily from the parent thread's `Skill` tool_use input, with the transcript's "Base directory for this skill:" sentinel line as fallback — and surface it as `type:skill` in the subagent dropdown / inline blocks and as an "Invoked skill" row in the right stats panel (hidden for plain agents). Mirrored across the JS and Tauri backends and the HTML export. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Ev7HGAZmHZccXPggTnusjA --- src-tauri/src/exporthtml.rs | 10 ++++- src-tauri/src/history.rs | 71 ++++++++++++++++++++++++++++++- src/main/export-assets/runtime.js | 3 +- src/main/exportHtml.js | 6 ++- src/main/history.js | 43 ++++++++++++++++++- src/renderer/conversations.js | 14 ++++-- src/shared/i18n-dict.js | 5 +++ test/history.test.js | 32 ++++++++++++++ 8 files changed, 174 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/exporthtml.rs b/src-tauri/src/exporthtml.rs index 617f0ff..2c2dbb7 100644 --- a/src-tauri/src/exporthtml.rs +++ b/src-tauri/src/exporthtml.rs @@ -268,7 +268,8 @@ fn read_subagents(file: &Path) -> Value { .ok() .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or(json!({})); - let shaped = shape_session(&parse_jsonl(&ent.path())); + let recs = parse_jsonl(&ent.path()); + let shaped = shape_session(&recs); let key = meta .get("toolUseId") .and_then(|v| v.as_str()) @@ -280,6 +281,7 @@ fn read_subagents(file: &Path) -> Value { "agentId": agent_id, "type": meta.get("agentType").or_else(|| meta.get("subagent_type")).and_then(|v| v.as_str()).unwrap_or("agent"), "description": meta.get("description").and_then(|v| v.as_str()).unwrap_or(""), + "skill": crate::history::skill_from_recs(&recs), "count": shaped.messages.len(), "totals": { "in": shaped.totals.0, "out": shaped.totals.1, "cacheRead": shaped.totals.2, "turns": shaped.totals.3 }, "messages": shaped.messages, @@ -381,7 +383,11 @@ pub fn build_data(file: &str) -> Value { if t.is_empty() { "(conversation)".to_string() } else { t } }; let stem = path.file_stem().and_then(|x| x.to_str()).unwrap_or(""); - let subagents = read_subagents(path); + let mut subagents = read_subagents(path); + if let Some(map) = subagents.as_object_mut() { + // The spawning Skill tool_use overrides the sentinel fallback (mirrors exportHtml.js). + crate::history::apply_skill_names(&s.messages, map); + } json!({ "meta": { "title": title, diff --git a/src-tauri/src/history.rs b/src-tauri/src/history.rs index 1ce2310..450dded 100644 --- a/src-tauri/src/history.rs +++ b/src-tauri/src/history.rs @@ -715,6 +715,71 @@ pub fn dir_stats(config: &Value) -> Vec { out } +/// A skill-forked subagent transcript opens with a sentinel user line +/// "Base directory for this skill: /" — the last path segment names the skill. +/// Fallback attribution only: the spawning `Skill` tool_use in the parent thread +/// (apply_skill_names) is authoritative and overrides this when present. (history.js skillFromRecs) +const SKILL_BASE_DIR_PREFIX: &str = "Base directory for this skill: "; +pub(crate) fn skill_from_recs(recs: &[Value]) -> Option { + let first = recs.iter().find(|r| { + r.get("type").and_then(|v| v.as_str()) == Some("user") + && r.get("message").is_some() + && !r.get("isMeta").and_then(|v| v.as_bool()).unwrap_or(false) + })?; + let text = content_text(first.get("message")?.get("content").unwrap_or(&Value::Null)); + // Only the opening prompt carries the sentinel — don't scan further user turns. + let rest = text.trim().strip_prefix(SKILL_BASE_DIR_PREFIX)?; + let line = rest.lines().next().unwrap_or("").trim(); + line.split(['/', '\\']).filter(|s| !s.is_empty()).last().map(|s| s.to_string()) +} + +/// Primary skill attribution (history.js applySkillNames): a subagent spawned by the `Skill` tool +/// is named by the spawning tool_use's input.skill (matched by tool_use id — the subagents map +/// key), in whichever thread the call lives (main or a nested subagent). Overrides the sentinel +/// fallback from skill_from_recs. +pub(crate) fn apply_skill_names(main_messages: &[Value], subs: &mut serde_json::Map) { + if subs.is_empty() { + return; + } + fn scan(msgs: &[Value], subs: &serde_json::Map, out: &mut Vec<(String, String)>) { + for m in msgs { + let blocks = match m.get("content").and_then(|c| c.as_array()) { + Some(b) => b, + None => continue, + }; + for b in blocks { + if b.get("type").and_then(|v| v.as_str()) != Some("tool_use") + || b.get("name").and_then(|v| v.as_str()) != Some("Skill") + { + continue; + } + let id = match b.get("id").and_then(|v| v.as_str()) { + Some(i) if subs.contains_key(i) => i, + _ => continue, + }; + if let Some(s) = b.get("input").and_then(|i| i.get("skill")).and_then(|v| v.as_str()) { + let s = s.trim(); + if !s.is_empty() { + out.push((id.to_string(), s.to_string())); + } + } + } + } + } + let mut named: Vec<(String, String)> = vec![]; + scan(main_messages, subs, &mut named); + for (_, v) in subs.iter() { + if let Some(msgs) = v.get("messages").and_then(|m| m.as_array()) { + scan(msgs, subs, &mut named); + } + } + for (id, name) in named { + if let Some(o) = subs.get_mut(&id).and_then(|s| s.as_object_mut()) { + o.insert("skill".into(), json!(name)); + } + } +} + /// Read a session's child subagent dialogues from `/subagents/agent-*.jsonl` (+ .meta.json), /// keyed by the spawning tool_use id so the renderer can nest them. {} when none. (history.js readSubagents) fn read_subagents(file: &str) -> serde_json::Map { @@ -765,6 +830,7 @@ fn read_subagents(file: &str) -> serde_json::Map { "file": ent.path().to_string_lossy(), "type": agent_type, "description": meta.get("description").and_then(|v| v.as_str()).unwrap_or(""), + "skill": skill_from_recs(&recs), "count": shaped.messages.len(), "totals": shaped.totals, "messages": shaped.messages, @@ -921,7 +987,8 @@ pub fn get_session(file: &str) -> Value { (true, Some(aid)) => format!("{}-{}", base_id, aid), _ => base_id.clone(), }; - let subs = if subagent { serde_json::Map::new() } else { read_subagents(file) }; + let mut subs = if subagent { serde_json::Map::new() } else { read_subagents(file) }; + apply_skill_names(&shaped.messages, &mut subs); let import_meta = read_import_meta(file); json!({ @@ -939,6 +1006,8 @@ pub fn get_session(file: &str) -> Value { "gitBranch": meta_rec.and_then(|r| r.get("gitBranch")).cloned().unwrap_or(Value::Null), "version": meta_rec.and_then(|r| r.get("version")).cloned().unwrap_or(Value::Null), "isSubagent": subagent, + // A standalone subagent transcript self-reports its invoking skill via the sentinel. + "skill": if subagent { skill_from_recs(&recs) } else { None:: }, "deleted": cc_deleted, "imported": import_meta.is_some(), "importedFrom": import_meta.as_ref().and_then(|m| m.get("originalPath")).cloned().unwrap_or(Value::Null), diff --git a/src/main/export-assets/runtime.js b/src/main/export-assets/runtime.js index f14b1e4..9f1b494 100644 --- a/src/main/export-assets/runtime.js +++ b/src/main/export-assets/runtime.js @@ -135,7 +135,8 @@ return '
' + ico + '' + esc(label) + '' + esc(target) + '' + badge + '' + inner + '
' + (sub ? renderSubagent(sub) : ''); } function renderSubagent(sub) { - return '
🤖子代理 · ' + esc(sub.type || 'agent') + '' + esc(sub.description || '') + '' + (sub.count || 0) + ' 条 · ' + fmtTok((sub.totals && sub.totals.out) || 0) + '↓
' + renderThread(sub.messages || []) + '
'; + var subName = (sub.type || 'agent') + (sub.skill ? ':' + sub.skill : ''); // skill-spawned agents carry the invoking skill + return '
🤖子代理 · ' + esc(subName) + '' + esc(sub.description || '') + '' + (sub.count || 0) + ' 条 · ' + fmtTok((sub.totals && sub.totals.out) || 0) + '↓
' + renderThread(sub.messages || []) + '
'; } function isReminder(t) { return /<(system-reminder|command-name|local-command)/.test(t); } diff --git a/src/main/exportHtml.js b/src/main/exportHtml.js index 1b82d65..d53fc17 100644 --- a/src/main/exportHtml.js +++ b/src/main/exportHtml.js @@ -133,17 +133,20 @@ function readSubagents(file) { const dir = path.join(path.dirname(file), path.basename(file, '.jsonl'), 'subagents'); let entries; try { entries = fs.readdirSync(dir); } catch (_) { return {}; } + const { skillFromRecs } = require('./history'); const byTool = {}; for (const name of entries) { if (!/^agent-.*\.jsonl$/.test(name)) continue; const agentId = name.replace(/^agent-/, '').replace(/\.jsonl$/, ''); let meta = {}; try { meta = JSON.parse(fs.readFileSync(path.join(dir, 'agent-' + agentId + '.meta.json'), 'utf8')); } catch (_) {} - const shaped = shapeSession(parseJsonl(path.join(dir, name))); + const recs = parseJsonl(path.join(dir, name)); + const shaped = shapeSession(recs); const sub = { agentId, type: meta.agentType || meta.subagent_type || 'agent', description: meta.description || '', + skill: skillFromRecs(recs), count: shaped.messages.length, totals: shaped.totals, messages: shaped.messages, @@ -205,6 +208,7 @@ function buildData(file) { const s = shapeSession(recs); const cwd = s.metaRec.cwd || null; const subagents = readSubagents(file); + require('./history').applySkillNames(s.messages, subagents); // spawning Skill tool_use overrides the sentinel fallback return { meta: { title: firstUserText(s.messages) || '(conversation)', diff --git a/src/main/history.js b/src/main/history.js index 7c64684..9a2dee4 100644 --- a/src/main/history.js +++ b/src/main/history.js @@ -166,6 +166,41 @@ function shapeMessages(recs) { return { messages, totals, model, firstTs, lastTs }; } +// A skill-forked subagent transcript opens with a sentinel user line +// "Base directory for this skill: /" — the last path segment names the skill. +// Fallback attribution only: the spawning `Skill` tool_use in the parent thread (applySkillNames) +// is authoritative and overrides this when present. +const SKILL_BASE_DIR_PREFIX = 'Base directory for this skill: '; +function skillFromRecs(recs) { + for (const r of recs) { + if (!r || r.type !== 'user' || !r.message || r.isMeta) continue; + const raw = contentText(r.message.content).trim(); + if (!raw.startsWith(SKILL_BASE_DIR_PREFIX)) return null; // only the opening prompt carries the sentinel + const line = raw.slice(SKILL_BASE_DIR_PREFIX.length).split('\n')[0].trim(); + const segs = line.split(/[\\/]/).filter(Boolean); + return segs.length ? segs[segs.length - 1] : null; + } + return null; +} + +// Primary skill attribution: a subagent spawned by the `Skill` tool is named by the spawning +// tool_use's input.skill (matched by tool_use id — the subagents map key), in whichever thread +// the call lives (main or a nested subagent). Overrides the sentinel fallback from skillFromRecs. +function applySkillNames(mainMessages, subs) { + const keys = Object.keys(subs); + if (!keys.length) return; + const scan = (msgs) => (msgs || []).forEach((m) => { + const blocks = m && Array.isArray(m.content) ? m.content : []; + for (const b of blocks) { + if (!b || b.type !== 'tool_use' || b.name !== 'Skill' || !subs[b.id]) continue; + const s = b.input && typeof b.input.skill === 'string' ? b.input.skill.trim() : ''; + if (s) subs[b.id].skill = s; + } + }); + scan(mainMessages); + for (const k of keys) scan(subs[k].messages); +} + // Read a session's subagent dialogues — //subagents/agent-.{jsonl,meta.json} // — keyed by the spawning Task/Agent tool_use id (agent-.meta.json's toolUseId), so the "对话" // view can nest each subagent's timeline under the call that spawned it. Mirrors the HTML export. @@ -182,13 +217,15 @@ function readSubagents(file) { try { meta = JSON.parse(fs.readFileSync(path.join(dir, 'agent-' + agentId + '.meta.json'), 'utf8')); } catch (_) {} let raw; try { raw = fs.readFileSync(path.join(dir, name), 'utf8'); } catch (_) { continue; } - const shaped = shapeMessages(parseLines(raw)); + const recs = parseLines(raw); + const shaped = shapeMessages(recs); const key = meta.toolUseId || ('agent:' + agentId); byTool[key] = { agentId, file: path.join(dir, name), // absolute path to this subagent's .jsonl (for "copy path") type: meta.agentType || meta.subagent_type || 'agent', description: meta.description || '', + skill: skillFromRecs(recs), count: shaped.messages.length, totals: shaped.totals, messages: shaped.messages, @@ -410,6 +447,7 @@ function createHistoryWatcher(opts) { // Only a top-level session embeds its child subagent dialogues (a subagent file has no nested // subagents/ dir of its own), so the renderer can nest them under their spawning Task call. const subagents = subagent ? {} : readSubagents(file); + applySkillNames(messages, subagents); const cwd = metaRec.cwd || null; const baseId = metaRec.sessionId || path.basename(file, '.jsonl'); const sessId = subagent && agentRec.agentId ? `${baseId}-${agentRec.agentId}` : baseId; @@ -428,6 +466,7 @@ function createHistoryWatcher(opts) { gitBranch: metaRec.gitBranch || null, version: metaRec.version || null, isSubagent: subagent, + skill: subagent ? skillFromRecs(recs) : null, // a standalone subagent transcript self-reports via the sentinel imported: !!imported, importedFrom: imported ? imported.originalPath : null, importedAt: imported ? imported.importedAt : null, @@ -598,4 +637,4 @@ function createHistoryWatcher(opts) { }; } -module.exports = { createHistoryWatcher, lineToMessage, firstUserText, readCcbud, decodeDirName, defaultDirs, subagentDir, readSubagentFiles, subagentTranscriptPaths }; +module.exports = { createHistoryWatcher, lineToMessage, firstUserText, readCcbud, decodeDirName, defaultDirs, subagentDir, readSubagentFiles, subagentTranscriptPaths, skillFromRecs, applySkillNames }; diff --git a/src/renderer/conversations.js b/src/renderer/conversations.js index 82bada6..60b6451 100644 --- a/src/renderer/conversations.js +++ b/src/renderer/conversations.js @@ -925,6 +925,9 @@ } // ---------- inline subagents (expand-at-call-site) ---------- + // Display name of a subagent: its agent type, suffixed with the skill that invoked it + // (`type:skill`) when the backend attributed one (Skill tool_use / transcript sentinel). + function subName(s) { return (s.type || 'agent') + (s.skill ? ':' + s.skill : ''); } // A subagent dialogue is keyed by the tool_use id that spawned it (history.readSubagents). We render it // as a lazily-filled disclosure directly under that call — at any nesting depth, since a subagent's own // tool cards run through this same path. Body stays empty until opened (see fillSubBody) to bound the DOM. @@ -936,7 +939,7 @@ const out = (s.totals && s.totals.out) || 0; const meta = `${esc(L('conv.subagentMsgs', { n: cnt }))} · ${fmtTok(out)}↓`; const desc = s.description ? ` · ${esc(s.description)}` : ''; - return `
🤖 ${esc(L('conv.subagent'))} · ${esc(s.type || 'agent')}${desc}${meta}
`; + return `
🤖 ${esc(L('conv.subagent'))} · ${esc(subName(s))}${desc}${meta}
`; } // Render one subagent's whole thread (recursively wiring its own inline subagents via renderMessage → // renderToolCard). idx=null so nested turns carry no data-mi (they're outside main-window navigation). @@ -971,7 +974,7 @@ const activeSub = !mainActive && subs[activeAgent] ? subs[activeAgent] : null; const seg = (active) => `inline-flex items-center gap-1.5 h-[28px] px-3 rounded-[8px] text-[12px] font-semibold cursor-pointer border transition-colors whitespace-nowrap ${active ? 'bg-brand-soft text-brand border-brand/25' : 'bg-bg-elev text-muted border-border-custom hover:text-fg hover:bg-chip-bg'}`; const mainTab = ``; - const ddLabel = activeSub ? `🤖 ${esc(activeSub.type || 'agent')}` : `🤖 ${esc(L('conv.stat.subagents'))} (${keys.length})`; + const ddLabel = activeSub ? `🤖 ${esc(subName(activeSub))}` : `🤖 ${esc(L('conv.stat.subagents'))} (${keys.length})`; const items = keys.map((k) => { const s = subs[k] || {}; const out = (s.totals && s.totals.out) || 0; @@ -979,7 +982,7 @@ const active = activeAgent === k; const desc = s.description ? `
${esc(s.description)}
` : ''; return ``; @@ -1078,9 +1081,14 @@ function renderSidePanels(detail) { const m = detail.meta || {}; const t = m.totals || {}; + // Invoking skill of the session in the panel: the active subagent's when one is selected, + // else the session's own (a standalone subagent transcript). Absent → row filtered out. + const panelSub = activeAgent !== 'main' && detail.subagents ? detail.subagents[activeAgent] : null; + const skill = panelSub ? panelSub.skill : m.skill; const rows = [ [L('conv.stat.title'), m.title], [L('conv.stat.model'), m.model], + [L('conv.stat.skill'), skill || null], ...(m.isSubagent ? [[L('conv.stat.type'), L('conv.subagentSession')]] : []), ...(m.imported ? [[L('conv.imported'), m.importedFrom || '✓']] : []), [L('conv.stat.project'), m.cwd ? projName(m.cwd) : m.project], diff --git a/src/shared/i18n-dict.js b/src/shared/i18n-dict.js index e113774..dcf1b20 100644 --- a/src/shared/i18n-dict.js +++ b/src/shared/i18n-dict.js @@ -338,6 +338,7 @@ 'conv.stat.subagents': 'Subagents', 'conv.stat.title': 'Title', 'conv.stat.model': 'Model', + 'conv.stat.skill': 'Invoked skill', 'conv.stat.type': 'Type', 'conv.stat.project': 'Project', 'conv.stat.branch': 'Branch', @@ -775,6 +776,7 @@ 'conv.stat.subagents': '子代理', 'conv.stat.title': '标题', 'conv.stat.model': '模型', + 'conv.stat.skill': '唤起 Skill', 'conv.stat.type': '类型', 'conv.stat.project': '项目', 'conv.stat.branch': '分支', @@ -1203,6 +1205,7 @@ 'conv.stat.subagents': '子代理', 'conv.stat.title': '標題', 'conv.stat.model': '模型', + 'conv.stat.skill': '喚起 Skill', 'conv.stat.type': '類型', 'conv.stat.project': '專案', 'conv.stat.branch': '分支', @@ -1640,6 +1643,7 @@ 'conv.stat.subagents': 'サブエージェント', 'conv.stat.title': 'タイトル', 'conv.stat.model': 'モデル', + 'conv.stat.skill': '起動スキル', 'conv.stat.type': '種類', 'conv.stat.project': 'プロジェクト', 'conv.stat.branch': 'ブランチ', @@ -2077,6 +2081,7 @@ 'conv.stat.subagents': '서브에이전트', 'conv.stat.title': '제목', 'conv.stat.model': '모델', + 'conv.stat.skill': '호출 스킬', 'conv.stat.type': '유형', 'conv.stat.project': '프로젝트', 'conv.stat.branch': '브랜치', diff --git a/test/history.test.js b/test/history.test.js index 33b6b34..1e9a861 100644 --- a/test/history.test.js +++ b/test/history.test.js @@ -98,6 +98,38 @@ try { check('subagentTranscriptPaths returns only the agent-*.jsonl (1)', subPaths.length === 1, `n=${subPaths.length}`); check('subagentTranscriptPaths excludes the .meta.json sidecar', subPaths.every((p) => /agent-aaa\.jsonl$/.test(p))); check('subagentTranscriptPaths are absolute paths', subPaths.every((p) => p.startsWith(subDir))); + + // ---- skill attribution (spawning Skill tool_use, with the transcript sentinel as fallback) ---- + const file2 = path.join(pdir, 'ef0bc8c9-86f0-4ca6-b89d-000000000002.jsonl'); + fs.writeFileSync(file2, + L({ type: 'user', sessionId: 'sk1', cwd: '/proj/x', uuid: 'v1', timestamp: '2026-06-19T00:00:00Z', message: { role: 'user', content: 'run the research skill' } }) + + L({ type: 'assistant', sessionId: 'sk1', cwd: '/proj/x', uuid: 'v2', timestamp: '2026-06-19T00:00:01Z', message: { id: 'msg_k1', role: 'assistant', model: 'glm-5.2', content: [{ type: 'tool_use', id: 'tu9', name: 'Skill', input: { skill: 'deep-research', args: 'topic' } }], usage: { input_tokens: 2, output_tokens: 1 } } }) + + L({ type: 'user', sessionId: 'sk1', cwd: '/proj/x', uuid: 'v3', message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tu9', content: 'done' }] }, toolUseResult: { agentId: 'bbb' } }) + ); + const subDir2 = path.join(pdir, path.basename(file2, '.jsonl'), 'subagents'); + fs.mkdirSync(subDir2, { recursive: true }); + // Sentinel says "dr-dir" but the spawning tool_use says "deep-research" — the call site wins. + fs.writeFileSync(path.join(subDir2, 'agent-bbb.jsonl'), + L({ type: 'user', isSidechain: true, sessionId: 'sk1', agentId: 'bbb', uuid: 'sb1', timestamp: '2026-06-19T00:00:02Z', message: { role: 'user', content: 'Base directory for this skill: /home/u/.claude/skills/dr-dir\n\ndo the research' } }) + + L({ type: 'assistant', isSidechain: true, sessionId: 'sk1', agentId: 'bbb', uuid: 'sb2', timestamp: '2026-06-19T00:00:03Z', message: { id: 'msg_k2', role: 'assistant', model: 'glm-5.2', content: [{ type: 'text', text: 'ok' }], usage: { input_tokens: 1, output_tokens: 1 } } }) + ); + fs.writeFileSync(path.join(subDir2, 'agent-bbb.meta.json'), JSON.stringify({ agentType: 'general-purpose', description: 'skill runner', toolUseId: 'tu9' })); + // No meta.json and no matching tool_use — only the sentinel (block content + Windows path) names it. + fs.writeFileSync(path.join(subDir2, 'agent-ccc.jsonl'), + L({ type: 'user', isSidechain: true, sessionId: 'sk1', agentId: 'ccc', uuid: 'sc1', timestamp: '2026-06-19T00:00:04Z', message: { role: 'user', content: [{ type: 'text', text: 'Base directory for this skill: C:\\Users\\u\\.claude\\skills\\pdf' }] } }) + ); + + const s3 = w.getSession(file2); + check('skill named by the spawning Skill tool_use (overrides sentinel)', !!s3.subagents.tu9 && s3.subagents.tu9.skill === 'deep-research', JSON.stringify(s3.subagents.tu9 && s3.subagents.tu9.skill)); + check('skill sentinel fallback when no Skill call resolves', !!s3.subagents['agent:ccc'] && s3.subagents['agent:ccc'].skill === 'pdf', JSON.stringify(s3.subagents['agent:ccc'] && s3.subagents['agent:ccc'].skill)); + check('plain Task subagent carries no skill', s2.subagents.tu1.skill == null, JSON.stringify(s2.subagents.tu1.skill)); + check('main session meta carries no skill', s3.meta.skill == null, JSON.stringify(s3.meta.skill)); + const s4 = w.getSession(path.join(subDir2, 'agent-bbb.jsonl')); + check('standalone subagent transcript self-reports its skill via sentinel', s4.meta.isSubagent === true && s4.meta.skill === 'dr-dir', JSON.stringify(s4.meta.skill)); + + // HTML export embeds the same attribution (exportHtml.js mirrors history.js). + const exp = require('../src/main/exportHtml').buildData(file2); + check('export data carries subagent skill', !!exp.subagents.tu9 && exp.subagents.tu9.skill === 'deep-research' && exp.subagents['agent:ccc'].skill === 'pdf', JSON.stringify(exp.subagents.tu9 && exp.subagents.tu9.skill)); } finally { fs.rmSync(root, { recursive: true, force: true }); } From 49c1da454a4b7767a671a0b2206d246bff070643 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 04:19:13 +0000 Subject: [PATCH 2/2] Bump version to 1.3.7. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Ev7HGAZmHZccXPggTnusjA --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 3a9ed9a..964a377 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ccbud", - "version": "1.3.6", + "version": "1.3.7", "description": "CC Buddy — Coding CLI Buddy. A cross-platform desktop app that proxies Claude Code to any Anthropic-compatible provider (one-click switching, model mapping) and browses your Claude Code & Codex session history.", "author": { "name": "loadchange", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ed8fcfb..ef567f4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -89,7 +89,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "app" -version = "1.3.6" +version = "1.3.7" dependencies = [ "arboard", "async-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3a415b7..74010e7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "1.3.6" +version = "1.3.7" description = "CCBuddy — Coding CLI Buddy" authors = ["loadchange "] license = "GPL-3.0-only" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index da0f81f..fb944b6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "CCBuddy", - "version": "1.3.6", + "version": "1.3.7", "identifier": "dev.ccbud.gateway", "build": { "frontendDist": "../src/renderer",