Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion packages/app/public/styles.css

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions packages/app/src/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,50 @@
}
}
}

/* Trace surfaces and token colors follow the existing theme toggle. */
@layer components {
.trace-list { --trace-surface: #f6f3e9; --trace-border: #e6e0d1; --trace-muted: #71695e; --trace-accent: #705000; --trace-code: #fcfbf6; color: #201e1a; }
.dark .trace-list { --trace-surface: #2c2924; --trace-border: #403b34; --trace-muted: #b2a897; --trace-accent: #f4d36c; --trace-code: #151410; color: #f5f1e8; }
.trace-list > * + * { margin-top: 1rem; }
.trace-message { min-width: 0; padding: 1rem 0; }
.trace-message h3 { color: var(--trace-accent); font-size: .7rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; margin-bottom: .65rem; }
.trace-user_message { border-left: 3px solid var(--trace-accent); background: var(--trace-surface); padding: 1rem 1.25rem; border-radius: 0 .6rem .6rem 0; }
.trace-prose { font-size: .875rem; line-height: 1.75; overflow-wrap: anywhere; }
.trace-line { white-space: pre-wrap; }
.trace-prose h4 { font-weight: 700; margin: .75rem 0 .25rem; }
.trace-prose code { background: var(--trace-surface); padding: .1rem .3rem; border-radius: .25rem; font-size: .85em; }
.trace-bullet { padding-left: 1.25rem; position: relative; }
.trace-bullet::before { content: '•'; position: absolute; left: .25rem; color: var(--trace-muted); }
.trace-event, .trace-structured { min-width: 0; border: 1px solid var(--trace-border); border-radius: .6rem; overflow: hidden; }
.trace-event summary, .trace-structured summary { cursor: pointer; padding: .75rem 1rem; font-size: .8rem; overflow-wrap: anywhere; }
.trace-event summary:focus-visible, .trace-structured summary:focus-visible { outline: 2px solid var(--trace-accent); outline-offset: -2px; }
.trace-event summary { background: var(--trace-surface); }
.trace-event-kind { display: inline-block; color: var(--trace-muted); font-size: .65rem; text-transform: uppercase; letter-spacing: .04em; margin-right: .75rem; }
.trace-meta { border-style: dashed; }
.trace-error { border-color: #dc2626; }
.trace-warning { border-color: #b7791f; }
.trace-structured { margin: .75rem 0; }
.trace-structured > div { padding: 0 1rem 1rem; }
.trace-code { min-width: 0; background: var(--trace-code); }
.trace-prose .trace-code { margin: .75rem 0; border: 1px solid var(--trace-border); border-radius: .5rem; overflow: hidden; }
.trace-code-label { padding: .4rem 1rem; border-bottom: 1px solid var(--trace-border); color: var(--trace-muted); font: 600 .65rem var(--font-mono); }
.trace-code pre { margin: 0; padding: 1rem; max-height: 24rem; overflow: auto; white-space: pre; font: .75rem/1.7 var(--font-mono); tab-size: 2; }
.trace-code code { background: none; padding: 0; font-size: inherit; }
.trace-token-keyword { color: #7e22ce; }
.trace-token-string { color: #166534; }
.trace-token-comment { color: #71695e; font-style: italic; }
.trace-token-number, .trace-token-variable { color: #075985; }
.dark .trace-token-keyword { color: #d8b4fe; }
.dark .trace-token-string { color: #86efac; }
.dark .trace-token-comment { color: #b2a897; }
.dark .trace-token-number, .dark .trace-token-variable { color: #7dd3fc; }
.trace-thinking { display: flex; align-items: center; gap: .5rem; color: var(--trace-muted); font-size: .8rem; padding: .25rem 0; }
.trace-spinner { width: .8rem; height: .8rem; border: 2px solid var(--trace-border); border-top-color: var(--trace-accent); border-radius: 50%; }
.trace-thinking-live { display: none; }
.trace-running .trace-at-end > .trace-thinking:last-child .trace-spinner { animation: trace-spin 1s linear infinite; }
.trace-running .trace-at-end > .trace-thinking:last-child .trace-thinking-live { display: inline; }
.trace-running .trace-at-end > .trace-thinking:last-child .trace-thinking-past { display: none; }
@keyframes trace-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .trace-running .trace-at-end > .trace-thinking:last-child .trace-spinner { animation: none; } }
}
49 changes: 49 additions & 0 deletions packages/app/src/trace-presentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/** Browser-only presentation, shared verbatim with tests. No runtime dependencies.
* Each bounded preview is scanned once; previously appended events are never re-rendered.
* This deliberately supports a small Markdown/XML subset, never arbitrary HTML.
*/
export const tracePresentationBrowserSource = String.raw`
const traceEscape=value=>String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const traceHighlight=(source,language='')=>{
const text=String(source),shell=/^(sh|bash|shell|zsh|console)$/.test(language),supported=shell||/^(js|javascript|ts|typescript|jsx|tsx|json|python|py|sql|css)$/.test(language);
if(!supported)return traceEscape(text);
const tokens=/"(?:[^"\\]|\\[\s\S])*(?:"|$)|'(?:[^'\\]|\\[\s\S])*(?:'|$)|#[^\n]*|\/\/[^\n]*|\$[A-Za-z_][A-Za-z_0-9]*|\b\d+(?:\.\d+)?\b|[A-Za-z_][A-Za-z_0-9]*|[^\w"'#$/]+|[\s\S]/g;
const keywords=new Set((shell?'if then else elif fi for in do done case esac function export local cd echo printf cat rg git npm curl sudo set exit': 'const let var function return async await import export from class new if else for while try catch throw true false null undefined def in and or not None True False select from where insert into values').split(' '));
return Array.from(text.matchAll(tokens),match=>{const token=match[0],kind=/^["']/.test(token)?'string':(shell?token[0]==='#':token.startsWith('//')||((language==='py'||language==='python')&&token[0]==='#'))?'comment':token[0]==='$'?'variable':/^\d/.test(token)?'number':keywords.has(token)?'keyword':'';return kind?'<span class="trace-token-'+kind+'">'+traceEscape(token)+'</span>':traceEscape(token)}).join('');
};
const traceCode=(text,language='')=>'<div class="trace-code">'+(language?'<div class="trace-code-label">'+traceEscape(language)+'</div>':'')+'<pre><code>'+traceHighlight(text,language)+'</code></pre></div>';
const traceInline=text=>{
const parts=text.split(/(\x60|\*\*)/),out=[];let code=false,bold=false;
const counts=parts.reduce((counts,part)=>{if(part==='\x60'||part==='**')counts[part]++;return counts},{'\x60':0,'**':0});
for(const part of parts){if(part==='\x60'){counts[part]--;if(code||counts[part]>0){out.push(code?'</code>':'<code>');code=!code}else out.push(part)}else if(part==='**'&&!code){counts[part]--;if(bold||counts[part]>0){out.push(bold?'</strong>':'<strong>');bold=!bold}else out.push(part)}else out.push(traceEscape(part))}
if(code)out.push('</code>');if(bold)out.push('</strong>');return out.join('');
};
const traceContent=input=>{
const lines=String(input).split(/\r?\n/),out=[];let fence='',language='',code=[],structured='';
const labels={task:'Task',instructions:'Instructions',environment_context:'Environment',system_reminder:'System reminder','system-reminder':'System reminder',INSTRUCTIONS:'Instructions',context:'Context'};
for(const line of lines){
const marker=line.match(/^\s*(\x60{3,}|~{3,})([^\s]*)\s*$/);
if(fence){if(marker&&marker[1][0]===fence[0]&&marker[1].length>=fence.length&&!marker[2]){out.push(traceCode(code.join('\n'),language));fence='';code=[]}else code.push(line);continue}
if(marker){fence=marker[1];language=marker[2].toLowerCase();continue}
const tag=line.trim().match(/^<(\/?)([\w-]+)>$/);
if(tag&&Object.hasOwn(labels,tag[2])){if(!structured&&!tag[1]){structured=tag[2];out.push('<details class="trace-structured"><summary>'+labels[structured]+'</summary><div>');continue}if(structured===tag[2]&&tag[1]){out.push('</div></details>');structured='';continue}}
const heading=line.match(/^#{1,6} (.*)$/),bullet=line.match(/^(?:[-*] |\d+\. )(.*)$/);
out.push(heading?'<h4>'+traceInline(heading[1])+'</h4>':bullet?'<div class="trace-bullet">'+traceInline(bullet[1])+'</div>':'<div class="trace-line">'+(traceInline(line)||'<br>')+'</div>');
}
if(fence)out.push(traceCode(code.join('\n'),language));if(structured)out.push('</div></details>');return out.join('');
};
const traceCard=e=>{
const type=e.type,title=traceEscape(e.title),preview=String(e.preview||'').slice(0,32768);
if(type==='reasoning')return '<div class="trace-thinking"><span class="trace-spinner" aria-hidden="true"></span><span class="trace-thinking-live">Thinking…</span><span class="trace-thinking-past">Thought</span></div>';
if(type==='user_message'||type==='assistant_message')return '<article class="trace-message trace-'+type+'"><h3>'+ (type==='user_message'?'You':'Assistant')+'</h3><div class="trace-prose">'+traceContent(preview)+'</div></article>';
const tool=['tool_call','tool_result','command','file_change'].includes(type);
let content=traceCode(preview);
if(type==='tool_call'){
let args;try{args=JSON.parse(preview)}catch{}
if(args&&typeof args==='object'&&!Array.isArray(args)){const command=args.command??args.cmd;content=typeof command==='string'?traceCode(command,'sh')+(Object.keys(args).length>1?traceCode(JSON.stringify(args,null,2),'json'):''):traceCode(JSON.stringify(args,null,2),'json')}else content=traceCode(preview,'json');
}
if(type==='command')content=traceCode(e.title,'sh')+traceCode(preview);
const kind=tool?'tool':type==='error'?'error':type==='warning'?'warning':'meta';
return '<details class="trace-event trace-'+kind+'"'+(type==='error'||type==='warning'?' open':'')+'><summary><span class="trace-event-kind">'+traceEscape(type.replaceAll('_',' '))+'</span><span>'+title+'</span></summary>'+content+'</details>';
};
`;
Loading
Loading