- 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
Unified · Split
| @@ -1,9 +1,6 @@ | ||
| 1 | 1 | ## TODO |
| 2 | 2 | |
| 3 | −- [x] Fix sort order for threads (newest first) | |
| 4 | −- [x] Indicate in thread list which threads are currently working | |
| 5 | −- [x] Logs should be per-thread, not a single events.txt | |
| 6 | −- [x] Audit the logs to identify which events are (and aren't) worth showing to the user, i.e. drop "preamble" and "work" events | |
| 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" | |
| 3 | +- [x] Remove model picker from new project menu | |
| 4 | +- [x] CMD+Shift+O to create new thread in current project | |
| 5 | +- [x] Remove project title pill from input box and topnav | |
| 6 | +- [x] Remove connection status from topnav | |
| @@ -32,13 +32,6 @@ function formatMessageMeta(createdAt: string, duration: string | null): string { · 31 unmodified lines | ||
| 32 | 32 | return `${formatTimestamp(createdAt)} • ${duration}`; |
| 33 | 33 | } |
| 34 | 34 | |
| 35 | −function 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 | − | |
| 42 | 35 | function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string { |
| 43 | 36 | if (tone === "error") return "text-rose-300/50"; |
| 44 | 37 | if (tone === "tool") return "text-[#8a8a8a]"; |
| @@ -69,13 +62,11 @@ export default function ChatView() { · 24 unmodified lines | ||
| 69 | 62 | ); |
| 70 | 63 | const phase = derivePhase(activeThread?.session ?? null); |
| 71 | 64 | const isWorking = phase === "running" || isSending || isConnecting; |
| 72 | − const workLogTurnId = | |
| 73 | − activeThread?.latestTurnId ?? activeThread?.session?.activeTurnId; | |
| 74 | 65 | const nowIso = new Date(nowTick).toISOString(); |
| 75 | 66 | const modelOptions = MODEL_OPTIONS; |
| 76 | 67 | const workLogEntries = useMemo( |
| 77 | − () => deriveWorkLogEntries(activeThread?.events ?? [], workLogTurnId), | |
| 78 | − [activeThread?.events, workLogTurnId], | |
| 68 | + () => deriveWorkLogEntries(activeThread?.events ?? [], undefined), | |
| 69 | + [activeThread?.events], | |
| 79 | 70 | ); |
| 80 | 71 | const assistantCompletionByItemId = useMemo(() => { |
| 81 | 72 | const map = new Map<string, string>(); |
| @@ -320,47 +311,8 @@ export default function ChatView() { · 238 unmodified lines | ||
| 320 | 311 | <h2 className="text-sm font-medium text-[#e0e0e0]"> |
| 321 | 312 | {activeThread.title} |
| 322 | 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 | 314 | </div> |
| 329 | 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 | 316 | {/* Diff toggle */} |
| 365 | 317 | <button |
| 366 | 318 | type="button" |
| @@ -418,7 +370,10 @@ export default function ChatView() { · 51 unmodified lines | ||
| 418 | 370 | {timelineEntry.entry.detail ? ( |
| 419 | 371 | <> |
| 420 | 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 | 377 | {timelineEntry.entry.detail} |
| 423 | 378 | </span> |
| 424 | 379 | </> |
| @@ -634,11 +589,6 @@ export default function ChatView() { · 209 unmodified lines | ||
| 634 | 589 | |
| 635 | 590 | {/* Right side: send / stop button */} |
| 636 | 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 | 592 | {phase === "running" ? ( |
| 643 | 593 | <button |
| 644 | 594 | type="button" |
| @@ -1,5 +1,5 @@ | ||
| 1 | −import { useMemo, useState } from "react"; | |
| 2 | −import { DEFAULT_MODEL, MODEL_OPTIONS, resolveModelSlug } from "../model-logic"; | |
| 1 | +import { useCallback, useEffect, useMemo, useState } from "react"; | |
| 2 | +import { DEFAULT_MODEL } from "../model-logic"; | |
| 3 | 3 | import { readNativeApi } from "../session-logic"; |
| 4 | 4 | import { useStore } from "../store"; |
| 5 | 5 | import type { Project } from "../types"; |
| @@ -27,45 +27,45 @@ export default function Sidebar() { · 21 unmodified lines | ||
| 27 | 27 | const api = useMemo(() => readNativeApi(), []); |
| 28 | 28 | const [addingProject, setAddingProject] = useState(false); |
| 29 | 29 | const [newCwd, setNewCwd] = useState(""); |
| 30 | − const [newModel, setNewModel] = useState(DEFAULT_MODEL); | |
| 31 | 30 | const [isPickingFolder, setIsPickingFolder] = useState(false); |
| 32 | 31 | |
| 33 | 32 | const handleAddProject = () => { |
| 34 | 33 | const cwd = newCwd.trim(); |
| 35 | 34 | if (!cwd) return; |
| 36 | 35 | const name = cwd.split("/").filter(Boolean).pop() ?? "project"; |
| 37 | − const normalizedModel = resolveModelSlug(newModel); | |
| 38 | 36 | const project: Project = { |
| 39 | 37 | id: crypto.randomUUID(), |
| 40 | 38 | name, |
| 41 | 39 | cwd, |
| 42 | − model: normalizedModel, | |
| 40 | + model: DEFAULT_MODEL, | |
| 43 | 41 | expanded: true, |
| 44 | 42 | }; |
| 45 | 43 | dispatch({ type: "ADD_PROJECT", project }); |
| 46 | 44 | setNewCwd(""); |
| 47 | − setNewModel(DEFAULT_MODEL); | |
| 48 | 45 | setAddingProject(false); |
| 49 | 46 | }; |
| 50 | 47 | |
| 51 | − const handleNewThread = (projectId: string) => { | |
| 52 | − dispatch({ | |
| 53 | − type: "ADD_THREAD", | |
| 54 | − thread: { | |
| 55 | − id: crypto.randomUUID(), | |
| 56 | − projectId, | |
| 57 | − title: "New thread", | |
| 58 | − model: | |
| 59 | − state.projects.find((p) => p.id === projectId)?.model ?? | |
| 60 | − DEFAULT_MODEL, | |
| 61 | − session: null, | |
| 62 | − messages: [], | |
| 63 | − events: [], | |
| 64 | − error: null, | |
| 65 | − createdAt: new Date().toISOString(), | |
| 66 | − }, | |
| 67 | − }); | |
| 68 | − }; | |
| 48 | + const handleNewThread = useCallback( | |
| 49 | + (projectId: string) => { | |
| 50 | + dispatch({ | |
| 51 | + type: "ADD_THREAD", | |
| 52 | + thread: { | |
| 53 | + id: crypto.randomUUID(), | |
| 54 | + projectId, | |
| 55 | + title: "New thread", | |
| 56 | + model: | |
| 57 | + state.projects.find((p) => p.id === projectId)?.model ?? | |
| 58 | + DEFAULT_MODEL, | |
| 59 | + session: null, | |
| 60 | + messages: [], | |
| 61 | + events: [], | |
| 62 | + error: null, | |
| 63 | + createdAt: new Date().toISOString(), | |
| 64 | + }, | |
| 65 | + }); | |
| 66 | + }, | |
| 67 | + [dispatch, state.projects], | |
| 68 | + ); | |
| 69 | 69 | |
| 70 | 70 | const handlePickFolder = async () => { |
| 71 | 71 | if (!api || isPickingFolder) return; |
| @@ -79,6 +79,45 @@ export default function Sidebar() { · 7 unmodified lines | ||
| 79 | 79 | } |
| 80 | 80 | }; |
| 81 | 81 | |
| 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 | 121 | return ( |
| 83 | 122 | <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-white/[0.08] bg-[#141414]"> |
| 84 | 123 | {/* Drag region / traffic light space */} |
| @@ -238,21 +277,6 @@ export default function Sidebar() { · 153 unmodified lines | ||
| 238 | 277 | {isPickingFolder ? "Picking folder..." : "Browse for folder"} |
| 239 | 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 | 280 | <div className="flex gap-2"> |
| 257 | 281 | <button |
| 258 | 282 | type="button" |
| @@ -161,6 +161,131 @@ describe("deriveWorkLogEntries", () => { · 160 unmodified lines | ||
| 161 | 161 | |
| 162 | 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 | }); |
| 165 | 290 | |
| 166 | 291 | describe("evolveSession", () => { |
| @@ -86,14 +86,20 @@ export type TimelineEntry = · 85 unmodified lines | ||
| 86 | 86 | entry: WorkLogEntry; |
| 87 | 87 | }; |
| 88 | 88 | |
| 89 | −function truncateLine(value: string, limit = 140): string { | |
| 90 | − if (value.length <= limit) return value; | |
| 91 | − return `${value.slice(0, limit - 1)}…`; | |
| 89 | +function normalizeDetail(value: string | undefined): string | undefined { | |
| 90 | + if (!value) return undefined; | |
| 91 | + const trimmed = value.trim(); | |
| 92 | + return trimmed.length > 0 ? trimmed : undefined; | |
| 92 | 93 | } |
| 93 | 94 | |
| 94 | 95 | function normalizeItemType(raw: string | undefined): string { |
| 95 | 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 | } |
| 98 | 104 | |
| 99 | 105 | function shouldDropItemType(type: string): boolean { |
| @@ -108,23 +114,30 @@ function shouldDropItemType(type: string): boolean { · 8 unmodified lines | ||
| 108 | 114 | return type === "work" || type.startsWith("work "); |
| 109 | 115 | } |
| 110 | 116 | |
| 117 | +function shouldShowItemLifecycle(type: string): boolean { | |
| 118 | + return ( | |
| 119 | + type.includes("tool") || | |
| 120 | + type.includes("command") || | |
| 121 | + type.includes("file change") | |
| 122 | + ); | |
| 123 | +} | |
| 124 | + | |
| 111 | 125 | function shouldDropMethod(method: string): boolean { |
| 112 | − return /(^|\/)(preamble|work)(\/|$)/i.test(method); | |
| 126 | + return /(^|\/)(preamble|work|reasoning|thought)(\/|$)/i.test(method); | |
| 113 | 127 | } |
| 114 | 128 | |
| 115 | 129 | function itemTypeMeta(type: string): { |
| 116 | 130 | label: string; |
| 117 | 131 | tone: WorkLogEntry["tone"]; |
| 118 | 132 | } { |
| 119 | − if ( | |
| 120 | − type.includes("tool") || | |
| 121 | − type.includes("command") || | |
| 122 | − type.includes("file change") | |
| 123 | − ) { | |
| 124 | − return { label: "Tool call", tone: "tool" }; | |
| 133 | + if (type.includes("command")) { | |
| 134 | + return { label: "Command run", tone: "tool" }; | |
| 135 | + } | |
| 136 | + if (type.includes("file change")) { | |
| 137 | + return { label: "File change", tone: "tool" }; | |
| 125 | 138 | } |
| 126 | − if (type.includes("agent message")) { | |
| 127 | − return { label: "Drafting response", tone: "thinking" }; | |
| 139 | + if (type.includes("tool")) { | |
| 140 | + return { label: "Tool call", tone: "tool" }; | |
| 128 | 141 | } |
| 129 | 142 | return { label: "Work item", tone: "info" }; |
| 130 | 143 | } |
| @@ -144,9 +157,8 @@ function extractDetail( · 13 unmodified lines | ||
| 144 | 157 | ]; |
| 145 | 158 | |
| 146 | 159 | for (const candidate of candidates) { |
| 147 | − if (candidate && candidate.trim().length > 0) { | |
| 148 | − return truncateLine(candidate.trim()); | |
| 149 | − } | |
| 160 | + const detail = normalizeDetail(candidate); | |
| 161 | + if (detail) return detail; | |
| 150 | 162 | } |
| 151 | 163 | return undefined; |
| 152 | 164 | } |
| @@ -158,6 +170,9 @@ function entryFromItemLifecycle(event: ProviderEvent): WorkLogEntry | null { · 5 unmodified lines | ||
| 158 | 170 | if (shouldDropItemType(normalizedType) || shouldDropMethod(event.method)) { |
| 159 | 171 | return null; |
| 160 | 172 | } |
| 173 | + if (!shouldShowItemLifecycle(normalizedType)) { | |
| 174 | + return null; | |
| 175 | + } | |
| 161 | 176 | |
| 162 | 177 | const meta = itemTypeMeta(normalizedType); |
| 163 | 178 | const isStarted = event.method === "item/started"; |
| @@ -234,6 +249,7 @@ function entryFromNotification( · 70 unmodified lines | ||
| 234 | 249 | const status = asString(turn?.status); |
| 235 | 250 | const turnError = asObject(turn?.error); |
| 236 | 251 | const turnErrorMessage = asString(turnError?.message); |
| 252 | + const turnErrorDetail = normalizeDetail(turnErrorMessage); | |
| 237 | 253 | const eventAt = Date.parse(event.createdAt); |
| 238 | 254 | const durationMs = |
| 239 | 255 | Number.isNaN(turnStartedAt) || |
| @@ -251,7 +267,7 @@ function entryFromNotification( · 11 unmodified lines | ||
| 251 | 267 | : durationMs !== undefined |
| 252 | 268 | ? `Turn complete in ${formatDuration(durationMs)}` |
| 253 | 269 | : "Turn complete", |
| 254 | − ...(turnErrorMessage ? { detail: truncateLine(turnErrorMessage) } : {}), | |
| 270 | + ...(turnErrorDetail ? { detail: turnErrorDetail } : {}), | |
| 255 | 271 | tone: status === "failed" ? "error" : "info", |
| 256 | 272 | }; |
| 257 | 273 | } |
| @@ -259,21 +275,7 @@ function entryFromNotification( · 1 unmodified line | ||
| 259 | 275 | const lifecycleEntry = entryFromItemLifecycle(event); |
| 260 | 276 | if (lifecycleEntry) return lifecycleEntry; |
| 261 | 277 | |
| 262 | − if (event.method.startsWith("item/")) { | |
| 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 | − } | |
| 278 | + if (event.method.startsWith("item/")) return null; | |
| 277 | 279 | |
| 278 | 280 | return null; |
| 279 | 281 | } |
| @@ -281,12 +283,13 @@ function entryFromNotification( · 1 unmodified line | ||
| 281 | 283 | function entryFromError(event: ProviderEvent): WorkLogEntry | null { |
| 282 | 284 | if (event.kind !== "error") return null; |
| 283 | 285 | if (shouldDropMethod(event.method)) return null; |
| 286 | + const detail = normalizeDetail(event.message); | |
| 284 | 287 | |
| 285 | 288 | return { |
| 286 | 289 | id: event.id, |
| 287 | 290 | createdAt: event.createdAt, |
| 288 | 291 | label: "Runtime error", |
| 289 | − ...(event.message ? { detail: truncateLine(event.message) } : {}), | |
| 292 | + ...(detail ? { detail } : {}), | |
| 290 | 293 | tone: "error", |
| 291 | 294 | }; |
| 292 | 295 | } |
| @@ -217,7 +217,7 @@ function reducer(state: AppState, action: Action): AppState { · 216 unmodified lines | ||
| 217 | 217 | event, |
| 218 | 218 | activeAssistantItemRef, |
| 219 | 219 | ), |
| 220 | − events: [event, ...t.events].slice(0, 200), | |
| 220 | + events: [event, ...t.events], | |
| 221 | 221 | error: |
| 222 | 222 | event.kind === "error" && event.message ? event.message : t.error, |
| 223 | 223 | ...updateTurnFields(t, event), |