Skip to content

Commit d2289f9

Browse files
feat(ui): message metadata, brighter text, native webview browser fix
Show model, tokens, and duration on each assistant response. Copy button always visible. Brighten all text colors and remove antialiasing that made text appear gray on macOS. Fix browser webview positioning to account for address bar height. Default browser URL to google.com.
1 parent 8dd9937 commit d2289f9

7 files changed

Lines changed: 118 additions & 22 deletions

File tree

crates/tauri-app/frontend/src/components/browser/BrowserPanel.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface BrowserPanelProps {
88

99
export function BrowserPanel(props: BrowserPanelProps) {
1010
const { store, setStore } = appStore;
11-
const currentUrl = () => store.threadBrowserUrls[props.threadId] || "https://example.com";
11+
const currentUrl = () => store.threadBrowserUrls[props.threadId] || "https://google.com";
1212
const [urlInput, setUrlInput] = createSignal(currentUrl());
1313
const [started, setStarted] = createSignal(false);
1414
let viewportRef: HTMLDivElement | undefined;
@@ -51,9 +51,9 @@ export function BrowserPanel(props: BrowserPanelProps) {
5151
});
5252
});
5353

54-
// Open the native webview on first mount
54+
// Open the native webview on first mount — delay to let layout settle
5555
onMount(() => {
56-
requestAnimationFrame(() => {
56+
setTimeout(() => {
5757
if (!viewportRef) return;
5858
const rect = viewportRef.getBoundingClientRect();
5959
const url = currentUrl();
@@ -69,7 +69,7 @@ export function BrowserPanel(props: BrowserPanelProps) {
6969
}).catch((e) => {
7070
console.error("Failed to open browser:", e);
7171
});
72-
});
72+
}, 100);
7373
});
7474

7575
// When started, keep bounds in sync

crates/tauri-app/frontend/src/components/chat/ChatArea.tsx

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -193,16 +193,27 @@ export function ChatArea() {
193193
);
194194
}
195195

196-
function MessageBubble(props: { msg: { id: string; role: string; content: string }; isGenerating: boolean; isLast: boolean }) {
196+
function MessageBubble(props: { msg: any; isGenerating: boolean; isLast: boolean }) {
197197
const [copied, setCopied] = createSignal(false);
198198
const isAssistant = () => props.msg.role === "assistant";
199+
const meta = () => props.msg.meta;
199200

200201
async function copyContent() {
201202
await navigator.clipboard.writeText(props.msg.content);
202203
setCopied(true);
203204
setTimeout(() => setCopied(false), 1500);
204205
}
205206

207+
function formatDuration(ms: number) {
208+
if (ms < 1000) return `${ms}ms`;
209+
return `${(ms / 1000).toFixed(1)}s`;
210+
}
211+
212+
function formatTokens(n: number) {
213+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
214+
return String(n);
215+
}
216+
206217
return (
207218
<div class={`message message-${props.msg.role}`}>
208219
<div class="message-content">
@@ -211,11 +222,26 @@ function MessageBubble(props: { msg: { id: string; role: string; content: string
211222
<Markdown content={props.msg.content} />
212223
</Show>
213224
</div>
214-
<Show when={isAssistant() && props.msg.content}>
215-
<button class="copy-btn" onClick={copyContent}>
216-
{copied() ? "Copied" : "Copy"}
217-
</button>
218-
</Show>
225+
<div class="message-footer">
226+
<Show when={isAssistant() && meta()}>
227+
<div class="message-meta">
228+
<Show when={meta()!.model}>
229+
<span class="meta-tag">{meta()!.model}</span>
230+
</Show>
231+
<Show when={meta()!.inputTokens != null || meta()!.outputTokens != null}>
232+
<span class="meta-tag">{formatTokens(meta()!.inputTokens || 0)} in / {formatTokens(meta()!.outputTokens || 0)} out</span>
233+
</Show>
234+
<Show when={meta()!.durationMs}>
235+
<span class="meta-tag">{formatDuration(meta()!.durationMs!)}</span>
236+
</Show>
237+
</div>
238+
</Show>
239+
<Show when={isAssistant() && props.msg.content}>
240+
<button class="copy-btn" onClick={copyContent}>
241+
{copied() ? "Copied" : "Copy"}
242+
</button>
243+
</Show>
244+
</div>
219245
</div>
220246
</div>
221247
);
@@ -380,16 +406,37 @@ if (!document.getElementById("chat-styles")) {
380406
padding: 4px 12px;
381407
}
382408
409+
.message-footer {
410+
display: flex;
411+
align-items: center;
412+
gap: 8px;
413+
min-height: 18px;
414+
}
415+
.message-meta {
416+
display: flex;
417+
align-items: center;
418+
gap: 6px;
419+
flex-wrap: wrap;
420+
}
421+
.meta-tag {
422+
font-size: 10px;
423+
font-weight: 500;
424+
color: var(--text-tertiary);
425+
background: var(--bg-muted);
426+
padding: 1px 6px;
427+
border-radius: 3px;
428+
font-family: var(--font-mono);
429+
white-space: nowrap;
430+
}
383431
.copy-btn {
384432
font-size: 11px;
385433
font-weight: 500;
386434
color: var(--text-tertiary);
387435
padding: 2px 8px;
388436
border-radius: var(--radius-sm);
389437
transition: all 0.12s;
390-
opacity: 0;
438+
margin-left: auto;
391439
}
392-
.message:hover .copy-btn { opacity: 1; }
393440
.copy-btn:hover { background: var(--bg-accent); color: var(--text-secondary); }
394441
395442
.typing-indicator {

crates/tauri-app/frontend/src/components/sidebar/ProjectGroup.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function ProjectGroup(props: { project: Project }) {
3939
setStore("contextMenu", { type: "project", id: props.project.id, x: e.clientX, y: e.clientY });
4040
}
4141

42-
const color = () => props.project.color || "var(--text-tertiary)";
42+
const color = () => props.project.color || "var(--text)";
4343

4444
return (
4545
<div
@@ -85,10 +85,10 @@ if (!document.getElementById("project-group-styles")) {
8585
transition: background 0.15s; text-align: left;
8686
}
8787
.project-toggle:hover { background: var(--bg-muted); }
88-
.collapse-icon { font-size: 8px; color: var(--text-tertiary); }
88+
.collapse-icon { font-size: 8px; color: var(--text-secondary); }
8989
.project-name { font-size: 10px; letter-spacing: 0.05em; }
9090
.project-add {
91-
font-size: 13px; color: var(--text-tertiary); padding: 4px 8px;
91+
font-size: 13px; color: var(--text-secondary); padding: 4px 8px;
9292
border-radius: var(--radius-sm); transition: background 0.15s;
9393
}
9494
.project-add:hover { background: var(--bg-accent); }

crates/tauri-app/frontend/src/stores/app-store.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,10 @@ function createAppStore() {
217217
}
218218
}
219219

220+
const turnStartTimes: Record<string, number> = {};
221+
220222
function handleAgentEvent(payload: AgentEventPayload) {
221223
const { thread_id, event_type } = payload;
222-
console.log(`[agent-event] ${event_type}`, event_type === "content_delta" ? payload.text?.slice(0, 40) : "");
223224

224225
switch (event_type) {
225226
case "content_delta": {
@@ -234,6 +235,7 @@ function createAppStore() {
234235
break;
235236
}
236237
case "turn_started":
238+
turnStartTimes[thread_id] = Date.now();
237239
setStore("sessionStatuses", thread_id, "generating");
238240
break;
239241
case "turn_completed":
@@ -256,6 +258,27 @@ function createAppStore() {
256258
{ id: crypto.randomUUID(), thread_id, role: "system" as const, content: `Aborted: ${payload.reason}` },
257259
]);
258260
break;
261+
case "usage_report": {
262+
const durationMs = turnStartTimes[thread_id] ? Date.now() - turnStartTimes[thread_id] : undefined;
263+
setStore("threadMessages", thread_id, (msgs) => {
264+
if (!msgs) return msgs;
265+
const last = msgs[msgs.length - 1];
266+
if (last && last.role === "assistant") {
267+
return [...msgs.slice(0, -1), {
268+
...last,
269+
meta: {
270+
model: payload.model,
271+
inputTokens: payload.input_tokens,
272+
outputTokens: payload.output_tokens,
273+
costUsd: payload.cost_usd,
274+
durationMs,
275+
},
276+
}];
277+
}
278+
return msgs;
279+
});
280+
break;
281+
}
259282
case "session_ready":
260283
setStore("sessionStatuses", thread_id, "ready");
261284
break;

crates/tauri-app/frontend/src/styles/global.css

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
--bg-hover: rgba(255, 255, 255, 0.04);
1212

1313
/* Text with warm undertones */
14-
--text: #e8e6f0;
15-
--text-secondary: #a8a6b8;
16-
--text-tertiary: #5c5a6a;
14+
--text: #f0eef8;
15+
--text-secondary: #b8b6c8;
16+
--text-tertiary: #807e92;
1717

1818
/* Accent palette — molten metals */
1919
--primary: #6b7cff;
@@ -64,8 +64,6 @@ body {
6464
user-select: none;
6565
-webkit-user-select: none;
6666
font-feature-settings: "ss01", "ss02", "cv01";
67-
-webkit-font-smoothing: antialiased;
68-
-moz-osx-font-smoothing: grayscale;
6967
}
7068

7169
#app {

crates/tauri-app/frontend/src/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,20 @@ export interface Thread {
1414
color: string | null;
1515
}
1616

17+
export interface MessageMeta {
18+
model?: string;
19+
inputTokens?: number;
20+
outputTokens?: number;
21+
durationMs?: number;
22+
costUsd?: number;
23+
}
24+
1725
export interface ChatMessage {
1826
id: string;
1927
thread_id: string;
2028
role: "user" | "assistant" | "system";
2129
content: string;
30+
meta?: MessageMeta;
2231
}
2332

2433
export interface AgentEventPayload {
@@ -31,6 +40,10 @@ export interface AgentEventPayload {
3140
message?: string;
3241
request_id?: string;
3342
description?: string;
43+
input_tokens?: number;
44+
output_tokens?: number;
45+
cost_usd?: number;
46+
model?: string;
3447
}
3548

3649
export type SessionStatus = "idle" | "starting" | "ready" | "generating" | "error";

crates/tauri-app/src/streaming.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ pub struct AgentEventPayload {
2525
pub request_id: Option<String>,
2626
#[serde(skip_serializing_if = "Option::is_none")]
2727
pub description: Option<String>,
28+
#[serde(skip_serializing_if = "Option::is_none")]
29+
pub input_tokens: Option<u64>,
30+
#[serde(skip_serializing_if = "Option::is_none")]
31+
pub output_tokens: Option<u64>,
32+
#[serde(skip_serializing_if = "Option::is_none")]
33+
pub cost_usd: Option<f64>,
34+
#[serde(skip_serializing_if = "Option::is_none")]
35+
pub model: Option<String>,
2836
}
2937

3038
pub fn spawn_event_forwarder(
@@ -143,7 +151,10 @@ pub fn spawn_event_forwarder(
143151
session_id: session_id.to_string(),
144152
thread_id: thread_id.to_string(),
145153
event_type: "usage_report".into(),
146-
text: Some(format!("{cost_usd:.6}")),
154+
input_tokens: Some(*input_tokens),
155+
output_tokens: Some(*output_tokens),
156+
cost_usd: Some(*cost_usd),
157+
model: Some(model.clone()),
147158
..default_payload()
148159
}
149160
}
@@ -179,5 +190,9 @@ fn default_payload() -> AgentEventPayload {
179190
message: None,
180191
request_id: None,
181192
description: None,
193+
input_tokens: None,
194+
output_tokens: None,
195+
cost_usd: None,
196+
model: None,
182197
}
183198
}

0 commit comments

Comments
 (0)