authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-02-08 23:09:54-08:00
committergravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-02-08 23:09:54-08:00
logde5df2690de58406a480071eb2c7ba7e2fd4e3bd
treef5c044df92808fd2001ea381dcc4f0b686967ab3
parent1bd823cd209fc67ef0bf6f31185e267fbdf8099a
signature Commit is signed but in an unrecognized format.

feat(renderer): simplify thread UI and reduce work-log noise

- remove model picker when creating projects and default to the project model - add Cmd+Shift+O shortcut to open a new thread in the active project - remove project/status pills from chat chrome for a cleaner layout - only surface actionable item lifecycle events (tool/command/file change) - add tests covering reasoning and agent-message filtering

6 files changed, 235 insertions(+), 136 deletions(-)

TODO.md+4-7
...@@ -1,9 +1,6 @@...@@ -1,9 +1,6 @@
1## TODO1## TODO
22
3- [x] Fix sort order for threads (newest first)3- [x] Remove model picker from new project menu
4- [x] Indicate in thread list which threads are currently working4- [x] CMD+Shift+O to create new thread in current project
5- [x] Logs should be per-thread, not a single events.txt5- [x] Remove project title pill from input box and topnav
6- [x] Audit the logs to identify which events are (and aren't) worth showing to the user, i.e. drop "preamble" and "work" events6- [x] Remove connection status from topnav
7- [x] Make the "end message" stand out from the work done prior (maybe just a bar between the two that says "worked for 10 minutes 31 seconds" or however long it was)
8- [x] Better logging of "time to complete" (I want this more than the timestamps tbh)
9- [x] Remove the left bars on messages to make UI feel "flatter"
apps/renderer/src/components/ChatView.tsx+6-56
...@@ -32,13 +32,6 @@ function formatMessageMeta(createdAt: string, duration: string | null): string {...@@ -32,13 +32,6 @@ function formatMessageMeta(createdAt: string, duration: string | null): string {
32 return `${formatTimestamp(createdAt)} • ${duration}`;32 return `${formatTimestamp(createdAt)} • ${duration}`;
33}33}
3434
35function statusLabel(phase: string): string {
36 if (phase === "running") return "Thinking / working";
37 if (phase === "connecting") return "Connecting";
38 if (phase === "ready") return "Ready";
39 return "Disconnected";
40}
41
42function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {35function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {
43 if (tone === "error") return "text-rose-300/50";36 if (tone === "error") return "text-rose-300/50";
44 if (tone === "tool") return "text-[#8a8a8a]";37 if (tone === "tool") return "text-[#8a8a8a]";
...@@ -69,13 +62,11 @@ export default function ChatView() {...@@ -69,13 +62,11 @@ export default function ChatView() {
69 );62 );
70 const phase = derivePhase(activeThread?.session ?? null);63 const phase = derivePhase(activeThread?.session ?? null);
71 const isWorking = phase === "running" || isSending || isConnecting;64 const isWorking = phase === "running" || isSending || isConnecting;
72 const workLogTurnId =
73 activeThread?.latestTurnId ?? activeThread?.session?.activeTurnId;
74 const nowIso = new Date(nowTick).toISOString();65 const nowIso = new Date(nowTick).toISOString();
75 const modelOptions = MODEL_OPTIONS;66 const modelOptions = MODEL_OPTIONS;
76 const workLogEntries = useMemo(67 const workLogEntries = useMemo(
77 () => deriveWorkLogEntries(activeThread?.events ?? [], workLogTurnId),68 () => deriveWorkLogEntries(activeThread?.events ?? [], undefined),
78 [activeThread?.events, workLogTurnId],69 [activeThread?.events],
79 );70 );
80 const assistantCompletionByItemId = useMemo(() => {71 const assistantCompletionByItemId = useMemo(() => {
81 const map = new Map<string, string>();72 const map = new Map<string, string>();
...@@ -320,47 +311,8 @@ export default function ChatView() {...@@ -320,47 +311,8 @@ export default function ChatView() {
320 <h2 className="text-sm font-medium text-[#e0e0e0]">311 <h2 className="text-sm font-medium text-[#e0e0e0]">
321 {activeThread.title}312 {activeThread.title}
322 </h2>313 </h2>
323 {activeProject && (
324 <span className="rounded-full bg-white/[0.06] px-2 py-0.5 text-[10px] text-[#a0a0a0]/50">
325 {activeProject.name}
326 </span>
327 )}
328 </div>314 </div>
329 <div className="flex items-center gap-3">315 <div className="flex items-center gap-3">
330 {/* Status indicator */}
331 <div
332 className={`inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-[10px] ${
333 phase === "running"
334 ? "border-sky-400/35 bg-sky-500/[0.08] text-sky-100"
335 : phase === "connecting"
336 ? "border-amber-400/35 bg-amber-500/[0.08] text-amber-100"
337 : phase === "ready"
338 ? "border-emerald-400/35 bg-emerald-500/[0.08] text-emerald-100"
339 : "border-white/[0.08] bg-white/[0.04] text-[#a0a0a0]/70"
340 }`}
341 >
342 <span className="relative inline-flex h-2.5 w-2.5">
343 {(phase === "running" || phase === "connecting") && (
344 <span
345 className={`absolute inline-flex h-full w-full animate-ping rounded-full ${
346 phase === "running" ? "bg-sky-300/70" : "bg-amber-300/70"
347 }`}
348 />
349 )}
350 <span
351 className={`relative inline-flex h-2.5 w-2.5 rounded-full ${
352 phase === "running"
353 ? "bg-sky-200"
354 : phase === "connecting"
355 ? "bg-amber-200"
356 : phase === "ready"
357 ? "bg-emerald-200"
358 : "bg-[#a0a0a0]/40"
359 }`}
360 />
361 </span>
362 <span>{statusLabel(phase)}</span>
363 </div>
364 {/* Diff toggle */}316 {/* Diff toggle */}
365 <button317 <button
366 type="button"318 type="button"
...@@ -418,7 +370,10 @@ export default function ChatView() {...@@ -418,7 +370,10 @@ export default function ChatView() {
418 {timelineEntry.entry.detail ? (370 {timelineEntry.entry.detail ? (
419 <>371 <>
420 {timelineEntry.entry.label}372 {timelineEntry.entry.label}
421 <span className="ml-1.5 font-mono text-[11px] opacity-60">373 <span
374 className="ml-1.5 inline-block max-w-[70ch] truncate align-bottom font-mono text-[11px] opacity-60"
375 title={timelineEntry.entry.detail}
376 >
422 {timelineEntry.entry.detail}377 {timelineEntry.entry.detail}
423 </span>378 </span>
424 </>379 </>
...@@ -634,11 +589,6 @@ export default function ChatView() {...@@ -634,11 +589,6 @@ export default function ChatView() {
634589
635 {/* Right side: send / stop button */}590 {/* Right side: send / stop button */}
636 <div className="flex items-center gap-2">591 <div className="flex items-center gap-2">
637 {activeProject && (
638 <span className="text-[11px] text-[#a0a0a0]/25">
639 {activeProject.name}
640 </span>
641 )}
642 {phase === "running" ? (592 {phase === "running" ? (
643 <button593 <button
644 type="button"594 type="button"
apps/renderer/src/components/Sidebar.tsx+63-39
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { useMemo, useState } from "react";1import { useCallback, useEffect, useMemo, useState } from "react";
2import { DEFAULT_MODEL, MODEL_OPTIONS, resolveModelSlug } from "../model-logic";2import { DEFAULT_MODEL } from "../model-logic";
3import { readNativeApi } from "../session-logic";3import { readNativeApi } from "../session-logic";
4import { useStore } from "../store";4import { useStore } from "../store";
5import type { Project } from "../types";5import type { Project } from "../types";
...@@ -27,45 +27,45 @@ export default function Sidebar() {...@@ -27,45 +27,45 @@ export default function Sidebar() {
27 const api = useMemo(() => readNativeApi(), []);27 const api = useMemo(() => readNativeApi(), []);
28 const [addingProject, setAddingProject] = useState(false);28 const [addingProject, setAddingProject] = useState(false);
29 const [newCwd, setNewCwd] = useState("");29 const [newCwd, setNewCwd] = useState("");
30 const [newModel, setNewModel] = useState(DEFAULT_MODEL);
31 const [isPickingFolder, setIsPickingFolder] = useState(false);30 const [isPickingFolder, setIsPickingFolder] = useState(false);
3231
33 const handleAddProject = () => {32 const handleAddProject = () => {
34 const cwd = newCwd.trim();33 const cwd = newCwd.trim();
35 if (!cwd) return;34 if (!cwd) return;
36 const name = cwd.split("/").filter(Boolean).pop() ?? "project";35 const name = cwd.split("/").filter(Boolean).pop() ?? "project";
37 const normalizedModel = resolveModelSlug(newModel);
38 const project: Project = {36 const project: Project = {
39 id: crypto.randomUUID(),37 id: crypto.randomUUID(),
40 name,38 name,
41 cwd,39 cwd,
42 model: normalizedModel,40 model: DEFAULT_MODEL,
43 expanded: true,41 expanded: true,
44 };42 };
45 dispatch({ type: "ADD_PROJECT", project });43 dispatch({ type: "ADD_PROJECT", project });
46 setNewCwd("");44 setNewCwd("");
47 setNewModel(DEFAULT_MODEL);
48 setAddingProject(false);45 setAddingProject(false);
49 };46 };
5047
51 const handleNewThread = (projectId: string) => {48 const handleNewThread = useCallback(
52 dispatch({49 (projectId: string) => {
53 type: "ADD_THREAD",50 dispatch({
54 thread: {51 type: "ADD_THREAD",
55 id: crypto.randomUUID(),52 thread: {
56 projectId,53 id: crypto.randomUUID(),
57 title: "New thread",54 projectId,
58 model:55 title: "New thread",
59 state.projects.find((p) => p.id === projectId)?.model ??56 model:
60 DEFAULT_MODEL,57 state.projects.find((p) => p.id === projectId)?.model ??
61 session: null,58 DEFAULT_MODEL,
62 messages: [],59 session: null,
63 events: [],60 messages: [],
64 error: null,61 events: [],
65 createdAt: new Date().toISOString(),62 error: null,
66 },63 createdAt: new Date().toISOString(),
67 });64 },
68 };65 });
66 },
67 [dispatch, state.projects],
68 );
6969
70 const handlePickFolder = async () => {70 const handlePickFolder = async () => {
71 if (!api || isPickingFolder) return;71 if (!api || isPickingFolder) return;
...@@ -79,6 +79,45 @@ export default function Sidebar() {...@@ -79,6 +79,45 @@ export default function Sidebar() {
79 }79 }
80 };80 };
8181
82 useEffect(() => {
83 const onWindowKeyDown = (event: KeyboardEvent) => {
84 const isNewThreadShortcut =
85 event.metaKey &&
86 event.shiftKey &&
87 !event.ctrlKey &&
88 !event.altKey &&
89 event.key.toLowerCase() === "o";
90 if (!isNewThreadShortcut) return;
91
92 const target = event.target;
93 if (target instanceof HTMLElement) {
94 const tagName = target.tagName;
95 if (
96 target.isContentEditable ||
97 tagName === "INPUT" ||
98 tagName === "TEXTAREA" ||
99 tagName === "SELECT"
100 ) {
101 return;
102 }
103 }
104
105 const activeThread = state.threads.find(
106 (t) => t.id === state.activeThreadId,
107 );
108 const projectId = activeThread?.projectId ?? state.projects[0]?.id;
109 if (!projectId) return;
110
111 event.preventDefault();
112 handleNewThread(projectId);
113 };
114
115 window.addEventListener("keydown", onWindowKeyDown);
116 return () => {
117 window.removeEventListener("keydown", onWindowKeyDown);
118 };
119 }, [handleNewThread, state.activeThreadId, state.projects, state.threads]);
120
82 return (121 return (
83 <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-white/[0.08] bg-[#141414]">122 <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-white/[0.08] bg-[#141414]">
84 {/* Drag region / traffic light space */}123 {/* Drag region / traffic light space */}
...@@ -238,21 +277,6 @@ export default function Sidebar() {...@@ -238,21 +277,6 @@ export default function Sidebar() {
238 {isPickingFolder ? "Picking folder..." : "Browse for folder"}277 {isPickingFolder ? "Picking folder..." : "Browse for folder"}
239 </button>278 </button>
240 )}279 )}
241 <select
242 className="mb-2 w-full rounded-md border border-white/[0.1] bg-white/[0.04] px-2 py-1.5 font-mono text-xs text-[#e0e0e0] placeholder:text-[#a0a0a0]/30 focus:border-white/30 focus:outline-none"
243 value={newModel}
244 onChange={(e) => setNewModel(e.target.value)}
245 onKeyDown={(e) => {
246 if (e.key === "Enter") handleAddProject();
247 if (e.key === "Escape") setAddingProject(false);
248 }}
249 >
250 {MODEL_OPTIONS.map((model) => (
251 <option key={model} value={model} className="bg-[#141414]">
252 {model}
253 </option>
254 ))}
255 </select>
256 <div className="flex gap-2">280 <div className="flex gap-2">
257 <button281 <button
258 type="button"282 type="button"
apps/renderer/src/session-logic.test.ts+125
...@@ -161,6 +161,131 @@ describe("deriveWorkLogEntries", () => {...@@ -161,6 +161,131 @@ describe("deriveWorkLogEntries", () => {
161161
162 expect(entries[0]?.label).toBe("Turn complete in 10s");162 expect(entries[0]?.label).toBe("Turn complete in 10s");
163 });163 });
164
165 it("hides reasoning and agent-message noise from the visible work log", () => {
166 const entries = deriveWorkLogEntries(
167 [
168 makeEvent({
169 id: "evt-turn-start",
170 method: "turn/started",
171 turnId: "turn-1",
172 createdAt: "2026-02-08T10:00:00.000Z",
173 }),
174 makeEvent({
175 id: "evt-summary-part",
176 method: "item/reasoning/summaryPartAdded",
177 turnId: "turn-1",
178 createdAt: "2026-02-08T10:00:01.000Z",
179 }),
180 makeEvent({
181 id: "evt-summary-delta",
182 method: "item/reasoning/summaryTextDelta",
183 turnId: "turn-1",
184 createdAt: "2026-02-08T10:00:02.000Z",
185 }),
186 makeEvent({
187 id: "evt-agent-start",
188 method: "item/started",
189 turnId: "turn-1",
190 createdAt: "2026-02-08T10:00:03.000Z",
191 payload: {
192 item: { id: "item-msg", type: "agentMessage", text: "Working..." },
193 },
194 }),
195 makeEvent({
196 id: "evt-agent-complete",
197 method: "item/completed",
198 turnId: "turn-1",
199 createdAt: "2026-02-08T10:00:04.000Z",
200 payload: {
201 item: {
202 id: "item-msg",
203 type: "agentMessage",
204 text: "Done response",
205 },
206 },
207 }),
208 makeEvent({
209 id: "evt-tool-start",
210 method: "item/started",
211 turnId: "turn-1",
212 createdAt: "2026-02-08T10:00:05.000Z",
213 payload: {
214 item: { id: "item-tool", type: "tool_call", command: "ls -la" },
215 },
216 }),
217 makeEvent({
218 id: "evt-tool-complete",
219 method: "item/completed",
220 turnId: "turn-1",
221 createdAt: "2026-02-08T10:00:06.000Z",
222 payload: {
223 item: { id: "item-tool", type: "tool_call", command: "ls -la" },
224 },
225 }),
226 makeEvent({
227 id: "evt-turn-complete",
228 method: "turn/completed",
229 turnId: "turn-1",
230 createdAt: "2026-02-08T10:00:08.000Z",
231 payload: { turn: { id: "turn-1", status: "completed" } },
232 }),
233 ],
234 "turn-1",
235 );
236
237 expect(entries.map((entry) => entry.label)).toEqual([
238 "Turn complete in 8.0s",
239 "Tool call complete",
240 "Tool call",
241 ]);
242 });
243
244 it("preserves full tool-call detail text without data truncation", () => {
245 const longCommand =
246 'node ./scripts/sync.js --project ct-round-5 --mode dry-run --include "very long argument with lots of detail and metadata to verify there is no hard truncation in session logic"';
247 const entries = deriveWorkLogEntries(
248 [
249 makeEvent({
250 id: "evt-tool-start",
251 method: "item/started",
252 turnId: "turn-1",
253 createdAt: "2026-02-08T10:00:05.000Z",
254 payload: {
255 item: { id: "item-tool", type: "tool_call", command: longCommand },
256 },
257 }),
258 ],
259 "turn-1",
260 );
261
262 expect(entries[0]?.detail).toBe(longCommand);
263 });
264
265 it("can derive work-log entries across all turns when not scoped", () => {
266 const entries = deriveWorkLogEntries(
267 [
268 makeEvent({
269 id: "evt-turn-1-tool",
270 method: "item/started",
271 turnId: "turn-1",
272 createdAt: "2026-02-08T10:00:01.000Z",
273 payload: { item: { type: "tool_call", command: "ls -la" } },
274 }),
275 makeEvent({
276 id: "evt-turn-2-tool",
277 method: "item/started",
278 turnId: "turn-2",
279 createdAt: "2026-02-08T10:00:02.000Z",
280 payload: { item: { type: "tool_call", command: "pwd" } },
281 }),
282 ],
283 undefined,
284 );
285
286 expect(entries).toHaveLength(2);
287 expect(entries.map((entry) => entry.detail)).toEqual(["pwd", "ls -la"]);
288 });
164});289});
165290
166describe("evolveSession", () => {291describe("evolveSession", () => {
apps/renderer/src/session-logic.ts+36-33
...@@ -86,14 +86,20 @@ export type TimelineEntry =...@@ -86,14 +86,20 @@ export type TimelineEntry =
86 entry: WorkLogEntry;86 entry: WorkLogEntry;
87 };87 };
8888
89function truncateLine(value: string, limit = 140): string {89function normalizeDetail(value: string | undefined): string | undefined {
90 if (value.length <= limit) return value;90 if (!value) return undefined;
91 return `${value.slice(0, limit - 1)}…`;91 const trimmed = value.trim();
92 return trimmed.length > 0 ? trimmed : undefined;
92}93}
9394
94function normalizeItemType(raw: string | undefined): string {95function normalizeItemType(raw: string | undefined): string {
95 if (!raw) return "item";96 if (!raw) return "item";
96 return raw.replace(/[_-]/g, " ").replace(/\s+/g, " ").trim().toLowerCase();97 return raw
98 .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
99 .replace(/[._/-]/g, " ")
100 .replace(/\s+/g, " ")
101 .trim()
102 .toLowerCase();
97}103}
98104
99function shouldDropItemType(type: string): boolean {105function shouldDropItemType(type: string): boolean {
...@@ -108,23 +114,30 @@ function shouldDropItemType(type: string): boolean {...@@ -108,23 +114,30 @@ function shouldDropItemType(type: string): boolean {
108 return type === "work" || type.startsWith("work ");114 return type === "work" || type.startsWith("work ");
109}115}
110116
117function shouldShowItemLifecycle(type: string): boolean {
118 return (
119 type.includes("tool") ||
120 type.includes("command") ||
121 type.includes("file change")
122 );
123}
124
111function shouldDropMethod(method: string): boolean {125function shouldDropMethod(method: string): boolean {
112 return /(^|\/)(preamble|work)(\/|$)/i.test(method);126 return /(^|\/)(preamble|work|reasoning|thought)(\/|$)/i.test(method);
113}127}
114128
115function itemTypeMeta(type: string): {129function itemTypeMeta(type: string): {
116 label: string;130 label: string;
117 tone: WorkLogEntry["tone"];131 tone: WorkLogEntry["tone"];
118} {132} {
119 if (133 if (type.includes("command")) {
120 type.includes("tool") ||134 return { label: "Command run", tone: "tool" };
121 type.includes("command") ||135 }
122 type.includes("file change")136 if (type.includes("file change")) {
123 ) {137 return { label: "File change", tone: "tool" };
124 return { label: "Tool call", tone: "tool" };
125 }138 }
126 if (type.includes("agent message")) {139 if (type.includes("tool")) {
127 return { label: "Drafting response", tone: "thinking" };140 return { label: "Tool call", tone: "tool" };
128 }141 }
129 return { label: "Work item", tone: "info" };142 return { label: "Work item", tone: "info" };
130}143}
...@@ -144,9 +157,8 @@ function extractDetail(...@@ -144,9 +157,8 @@ function extractDetail(
144 ];157 ];
145158
146 for (const candidate of candidates) {159 for (const candidate of candidates) {
147 if (candidate && candidate.trim().length > 0) {160 const detail = normalizeDetail(candidate);
148 return truncateLine(candidate.trim());161 if (detail) return detail;
149 }
150 }162 }
151 return undefined;163 return undefined;
152}164}
...@@ -158,6 +170,9 @@ function entryFromItemLifecycle(event: ProviderEvent): WorkLogEntry | null {...@@ -158,6 +170,9 @@ function entryFromItemLifecycle(event: ProviderEvent): WorkLogEntry | null {
158 if (shouldDropItemType(normalizedType) || shouldDropMethod(event.method)) {170 if (shouldDropItemType(normalizedType) || shouldDropMethod(event.method)) {
159 return null;171 return null;
160 }172 }
173 if (!shouldShowItemLifecycle(normalizedType)) {
174 return null;
175 }
161176
162 const meta = itemTypeMeta(normalizedType);177 const meta = itemTypeMeta(normalizedType);
163 const isStarted = event.method === "item/started";178 const isStarted = event.method === "item/started";
...@@ -234,6 +249,7 @@ function entryFromNotification(...@@ -234,6 +249,7 @@ function entryFromNotification(
234 const status = asString(turn?.status);249 const status = asString(turn?.status);
235 const turnError = asObject(turn?.error);250 const turnError = asObject(turn?.error);
236 const turnErrorMessage = asString(turnError?.message);251 const turnErrorMessage = asString(turnError?.message);
252 const turnErrorDetail = normalizeDetail(turnErrorMessage);
237 const eventAt = Date.parse(event.createdAt);253 const eventAt = Date.parse(event.createdAt);
238 const durationMs =254 const durationMs =
239 Number.isNaN(turnStartedAt) ||255 Number.isNaN(turnStartedAt) ||
...@@ -251,7 +267,7 @@ function entryFromNotification(...@@ -251,7 +267,7 @@ function entryFromNotification(
251 : durationMs !== undefined267 : durationMs !== undefined
252 ? `Turn complete in ${formatDuration(durationMs)}`268 ? `Turn complete in ${formatDuration(durationMs)}`
253 : "Turn complete",269 : "Turn complete",
254 ...(turnErrorMessage ? { detail: truncateLine(turnErrorMessage) } : {}),270 ...(turnErrorDetail ? { detail: turnErrorDetail } : {}),
255 tone: status === "failed" ? "error" : "info",271 tone: status === "failed" ? "error" : "info",
256 };272 };
257 }273 }
...@@ -259,21 +275,7 @@ function entryFromNotification(...@@ -259,21 +275,7 @@ function entryFromNotification(
259 const lifecycleEntry = entryFromItemLifecycle(event);275 const lifecycleEntry = entryFromItemLifecycle(event);
260 if (lifecycleEntry) return lifecycleEntry;276 if (lifecycleEntry) return lifecycleEntry;
261277
262 if (event.method.startsWith("item/")) {278 if (event.method.startsWith("item/")) return null;
263 const payload = asObject(event.payload);
264 const item = asObject(payload?.item);
265 const normalizedType = normalizeItemType(asString(item?.type));
266 if (shouldDropItemType(normalizedType)) {
267 return null;
268 }
269
270 return {
271 id: event.id,
272 createdAt: event.createdAt,
273 label: `Event: ${event.method.replace(/^item\//, "")}`,
274 tone: "info",
275 };
276 }
277279
278 return null;280 return null;
279}281}
...@@ -281,12 +283,13 @@ function entryFromNotification(...@@ -281,12 +283,13 @@ function entryFromNotification(
281function entryFromError(event: ProviderEvent): WorkLogEntry | null {283function entryFromError(event: ProviderEvent): WorkLogEntry | null {
282 if (event.kind !== "error") return null;284 if (event.kind !== "error") return null;
283 if (shouldDropMethod(event.method)) return null;285 if (shouldDropMethod(event.method)) return null;
286 const detail = normalizeDetail(event.message);
284287
285 return {288 return {
286 id: event.id,289 id: event.id,
287 createdAt: event.createdAt,290 createdAt: event.createdAt,
288 label: "Runtime error",291 label: "Runtime error",
289 ...(event.message ? { detail: truncateLine(event.message) } : {}),292 ...(detail ? { detail } : {}),
290 tone: "error",293 tone: "error",
291 };294 };
292}295}
apps/renderer/src/store.ts+1-1
...@@ -217,7 +217,7 @@ function reducer(state: AppState, action: Action): AppState {...@@ -217,7 +217,7 @@ function reducer(state: AppState, action: Action): AppState {
217 event,217 event,
218 activeAssistantItemRef,218 activeAssistantItemRef,
219 ),219 ),
220 events: [event, ...t.events].slice(0, 200),220 events: [event, ...t.events],
221 error:221 error:
222 event.kind === "error" && event.message ? event.message : t.error,222 event.kind === "error" && event.message ? event.message : t.error,
223 ...updateTurnFields(t, event),223 ...updateTurnFields(t, event),