feat: log viewer, expandable work log errors, and custom accent presets#18
feat: log viewer, expandable work log errors, and custom accent presets#18aaditagrawal merged 8 commits intomainfrom
Conversation
- Add server-side WS API for logs (logs.getDir, logs.list, logs.read) so log viewing works in both web and desktop modes - Settings Logs section: show log directory path, "Show in File Manager" (desktop only), and in-app log viewer with file selector - Desktop IPC: getLogDir, listLogFiles, readLogFile, openLogDir channels - Work log entries are now click-to-expand when truncated, showing the full error/detail text instead of requiring hover
Colorize key=value log tokens: timestamps (dim), levels (blue/amber/red by severity), fiber IDs (violet), and message content (foreground). Uses inline regex parsing — no external dependency.
- Add customAccentPresets array to app settings (persisted in localStorage) - Custom presets appear alongside built-in presets with an x to remove - "Save as Preset" button in the custom color row prompts for a name - Move Reset and Save as Preset to the right side of the color picker row
Use a themed inline text input + Save button instead of window.prompt() for naming custom accent color presets. Escape or blur cancels.
📝 WalkthroughWalkthroughAdds log-access features across desktop, server, and web: new IPC channels and preload APIs for desktop, WebSocket methods and handlers on server, a web-native API and Settings log viewer with highlighting, plus minor docs and settings schema updates and an updated bug report template. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Web UI (Settings)
participant Api as WsNativeApi
participant Transport as WS Transport
participant Server as App Server
participant FS as File System
UI->>Api: logs.list()
Api->>Transport: request logsList
Transport->>Server: logsList request
Server->>FS: read log directory
FS-->>Server: list of .log files
Server-->>Transport: files list
Transport-->>Api: files list
Api-->>UI: files list
sequenceDiagram
participant Renderer as Desktop Renderer
participant Preload as Preload Bridge
participant Main as Main Process
participant FS2 as File System / Shell
Renderer->>Preload: invoke listLogFiles()
Preload->>Main: ipc invoke desktop:log-list
Main->>FS2: read LOG_DIR, filter *.log
FS2-->>Main: file list
Main-->>Preload: file list
Preload-->>Renderer: file list
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (5)
apps/web/src/appSettings.ts (1)
106-115: Consider adding length constraints for consistency.Other string fields in this schema use
isMaxLengthchecks (e.g.,accentColorhasisMaxLength(16), paths haveisMaxLength(4096)). Thelabelandvaluefields incustomAccentPresetslack these constraints.💡 Optional: Add length constraints
customAccentPresets: Schema.Array( Schema.Struct({ - label: Schema.String, - value: Schema.String, + label: Schema.String.check(Schema.isMaxLength(64)), + value: Schema.String.check(Schema.isMaxLength(16)), }), ).pipe(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/appSettings.ts` around lines 106 - 115, customAccentPresets schema entries for label and value currently use plain Schema.String; add length constraints using Schema.isMaxLength to match the rest of the file (e.g., wrap Schema.String for both label and value with Schema.isMaxLength(16) or another appropriate limit) so the Schema.Struct in customAccentPresets enforces max lengths consistently; update the Schema.Struct that defines label and value to use the constrained string schema.apps/web/src/components/chat/MessagesTimeline.tsx (1)
754-757: Consider adding keyboard accessibility for the clickable container.The clickable div lacks keyboard navigation support. Users relying on keyboard navigation cannot toggle expansion.
♿ Add keyboard accessibility
<div - className={cn("rounded-lg px-1 py-1", isExpandable && "cursor-pointer")} + className={cn("rounded-lg px-1 py-1", isExpandable && "cursor-pointer")} + role={isExpandable ? "button" : undefined} + tabIndex={isExpandable ? 0 : undefined} onClick={isExpandable ? () => setExpanded((prev) => !prev) : undefined} + onKeyDown={ + isExpandable + ? (e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setExpanded((prev) => !prev); + } + } + : undefined + } >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/chat/MessagesTimeline.tsx` around lines 754 - 757, The clickable container in MessagesTimeline lacks keyboard accessibility; update the div that uses isExpandable and setExpanded to behave like a button by adding tabIndex={0}, role="button", and aria-expanded={expanded} (or aria-expanded based on setExpanded state), and implement an onKeyDown handler that toggles expansion when Enter or Space is pressed (calling the same toggle logic used in onClick) while preventing default for Space to avoid page scroll; ensure these changes reference the existing isExpandable, setExpanded, and expanded state variables so keyboard users can toggle the container.apps/web/src/routes/_chat.settings.tsx (3)
1358-1382: Consider extracting the log file read logic to reduce duplication.The file reading and scroll-to-bottom logic is duplicated between the
onValueChangehandler (lines 1331-1344) and the Refresh buttononClick(lines 1362-1378). Extracting this to a shared helper would improve maintainability.💡 Optional: Extract shared logic
const loadLogFile = useCallback(async (filename: string) => { setIsLoadingLogs(true); try { const api = ensureNativeApi(); const result = await api.logs.read(filename); setLogContent(result.content); requestAnimationFrame(() => { if (logViewerRef.current) { logViewerRef.current.scrollTop = logViewerRef.current.scrollHeight; } }); } catch { setLogContent("Failed to read log file."); } finally { setIsLoadingLogs(false); } }, []);Then use
loadLogFile(value)in both handlers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/routes/_chat.settings.tsx` around lines 1358 - 1382, Extract the duplicated log-reading and scroll-to-bottom logic used in the onValueChange handler and the Refresh Button onClick into a shared async helper (e.g., loadLogFile) and call that from both places; the helper should accept the filename, call ensureNativeApi() and api.logs.read(filename), setIsLoadingLogs(true)/false around the call, setLogContent(result.content) or the failure message on error, and perform the requestAnimationFrame scroll logic using logViewerRef.current to scroll to scrollHeight so both handlers reuse the same code.
670-676: Consider validating for duplicate preset names before saving.The form submits the new preset without checking if a preset with the same name and value already exists in
customAccentPresets. While the "Save as Preset" button is hidden when the color matches an existing preset, this relies on color matching only—a user could potentially create presets with duplicate names.💡 Optional: Add duplicate name validation
onSubmit={(e) => { e.preventDefault(); const name = presetNameInput.trim(); if (!name) return; + const isDuplicate = settings.customAccentPresets.some( + (p) => p.label.toLowerCase() === name.toLowerCase() + ); + if (isDuplicate) return; updateSettings({ customAccentPresets: [ ...settings.customAccentPresets, { label: name, value: accentColor }, ], }); setPresetNameInput(null); }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/routes/_chat.settings.tsx` around lines 670 - 676, Before calling updateSettings to append the new preset, check customAccentPresets for an existing preset with the same label (and optionally same value) and prevent adding duplicates; if a matching preset is found, do not call updateSettings and instead handle validation (e.g., show a validation error or clear the input) via setPresetNameInput or existing state. Update the logic around updateSettings, customAccentPresets and setPresetNameInput to perform this duplicate-name (and optional name+value) check and early-return/notify instead of blindly pushing the new preset.
600-614: Consider using a separate clickable element for better accessibility.The delete control uses
<span role="button">nested inside a<button>. Whilerole="button"provides some accessibility, it lacks keyboard focusability and doesn't receive keyboard events automatically. However, since nesting a<button>inside a<button>is invalid HTML, the current approach is a reasonable compromise for this hover-reveal pattern.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/routes/_chat.settings.tsx` around lines 600 - 614, Replace the non-focusable span "button" with a real focusable control: make the delete control a standalone <button type="button"> (with the same aria-label, className and onClick behavior that calls updateSettings and stops propagation) rather than using role="button" on a span; ensure it is not nested inside another <button> (refactor the parent markup if needed) and keep keyboard activation semantics (Enter/Space) by using the native button element; reference updateSettings, settings.customAccentPresets, preset.value and preset.label to locate and preserve the remove logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/web/src/appSettings.ts`:
- Around line 106-115: customAccentPresets schema entries for label and value
currently use plain Schema.String; add length constraints using
Schema.isMaxLength to match the rest of the file (e.g., wrap Schema.String for
both label and value with Schema.isMaxLength(16) or another appropriate limit)
so the Schema.Struct in customAccentPresets enforces max lengths consistently;
update the Schema.Struct that defines label and value to use the constrained
string schema.
In `@apps/web/src/components/chat/MessagesTimeline.tsx`:
- Around line 754-757: The clickable container in MessagesTimeline lacks
keyboard accessibility; update the div that uses isExpandable and setExpanded to
behave like a button by adding tabIndex={0}, role="button", and
aria-expanded={expanded} (or aria-expanded based on setExpanded state), and
implement an onKeyDown handler that toggles expansion when Enter or Space is
pressed (calling the same toggle logic used in onClick) while preventing default
for Space to avoid page scroll; ensure these changes reference the existing
isExpandable, setExpanded, and expanded state variables so keyboard users can
toggle the container.
In `@apps/web/src/routes/_chat.settings.tsx`:
- Around line 1358-1382: Extract the duplicated log-reading and scroll-to-bottom
logic used in the onValueChange handler and the Refresh Button onClick into a
shared async helper (e.g., loadLogFile) and call that from both places; the
helper should accept the filename, call ensureNativeApi() and
api.logs.read(filename), setIsLoadingLogs(true)/false around the call,
setLogContent(result.content) or the failure message on error, and perform the
requestAnimationFrame scroll logic using logViewerRef.current to scroll to
scrollHeight so both handlers reuse the same code.
- Around line 670-676: Before calling updateSettings to append the new preset,
check customAccentPresets for an existing preset with the same label (and
optionally same value) and prevent adding duplicates; if a matching preset is
found, do not call updateSettings and instead handle validation (e.g., show a
validation error or clear the input) via setPresetNameInput or existing state.
Update the logic around updateSettings, customAccentPresets and
setPresetNameInput to perform this duplicate-name (and optional name+value)
check and early-return/notify instead of blindly pushing the new preset.
- Around line 600-614: Replace the non-focusable span "button" with a real
focusable control: make the delete control a standalone <button type="button">
(with the same aria-label, className and onClick behavior that calls
updateSettings and stops propagation) rather than using role="button" on a span;
ensure it is not nested inside another <button> (refactor the parent markup if
needed) and keep keyboard activation semantics (Enter/Space) by using the native
button element; reference updateSettings, settings.customAccentPresets,
preset.value and preset.label to locate and preserve the remove logic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5608475b-df68-485b-9d45-08a5247ef6e2
📒 Files selected for processing (11)
.github/ISSUE_TEMPLATE/bug_report.ymlAGENTS.mdapps/desktop/src/main.tsapps/desktop/src/preload.tsapps/server/src/wsServer.tsapps/web/src/appSettings.tsapps/web/src/components/chat/MessagesTimeline.tsxapps/web/src/routes/_chat.settings.tsxapps/web/src/wsNativeApi.tspackages/contracts/src/ipc.tspackages/contracts/src/ws.ts
- Add maxLength constraints to customAccentPresets schema (64 for label, 16 for value) - Add keyboard accessibility to expandable work log entries (role, tabIndex, Enter/Space, aria-expanded) - Extract loadLogFile helper to deduplicate log reading + scroll logic - Add duplicate preset name validation (case-insensitive) - Replace nested span[role=button] with proper button elements for preset delete
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/web/src/routes/_chat.settings.tsx (1)
706-718: Consider addingmaxLengthto enforce schema constraint.The PR summary mentions
maxLengthconstraints were added to the schema (label: 64 characters). AddingmaxLength={64}to the input provides immediate feedback and prevents users from typing beyond the limit.✨ Proposed enhancement
<Input ref={presetNameRef} className="h-7 w-32 py-0 text-xs leading-7" placeholder="Preset name" value={presetNameInput} + maxLength={64} onChange={(e) => setPresetNameInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Escape") setPresetNameInput(null); }} onBlur={() => { if (!presetNameInput.trim()) setPresetNameInput(null); }} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/routes/_chat.settings.tsx` around lines 706 - 718, The Input for preset names (the component using ref={presetNameRef}, value={presetNameInput}, onChange={(e) => setPresetNameInput(e.target.value)}) should enforce the schema max length: add maxLength={64} to the Input props and ensure onChange trims or blocks extra characters (e.g., only call setPresetNameInput with e.target.value.slice(0,64)) so the UI prevents typing beyond the 64-char schema constraint and stays in sync with onBlur/onKeyDown logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/src/routes/_chat.settings.tsx`:
- Around line 342-348: The current useEffect calls ensureNativeApi()
synchronously which can throw when the native API is unavailable; wrap the
ensureNativeApi() call in a try-catch inside the useEffect (around the call to
ensureNativeApi and subsequent api.logs.getDir() promise) so any synchronous
exception is caught and ignored or handled, and only call api.logs.getDir() when
ensureNativeApi() succeeds; update the effect that references ensureNativeApi(),
api.logs.getDir(), and setLogDir accordingly.
---
Nitpick comments:
In `@apps/web/src/routes/_chat.settings.tsx`:
- Around line 706-718: The Input for preset names (the component using
ref={presetNameRef}, value={presetNameInput}, onChange={(e) =>
setPresetNameInput(e.target.value)}) should enforce the schema max length: add
maxLength={64} to the Input props and ensure onChange trims or blocks extra
characters (e.g., only call setPresetNameInput with e.target.value.slice(0,64))
so the UI prevents typing beyond the 64-char schema constraint and stays in sync
with onBlur/onKeyDown logic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b6929142-f7d6-4d78-886e-cda852de2b72
📒 Files selected for processing (3)
apps/web/src/appSettings.tsapps/web/src/components/chat/MessagesTimeline.tsxapps/web/src/routes/_chat.settings.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/src/appSettings.ts
| useEffect(() => { | ||
| const api = ensureNativeApi(); | ||
| void api.logs | ||
| .getDir() | ||
| .then((result) => setLogDir(result.dir)) | ||
| .catch(() => {}); | ||
| }, []); |
There was a problem hiding this comment.
Uncaught exception when native API is unavailable.
ensureNativeApi() throws synchronously if the native API is not found. Since the call is outside the .catch() chain, the exception won't be caught when running in web-only mode.
🛡️ Proposed fix to wrap in try-catch
useEffect(() => {
+ try {
const api = ensureNativeApi();
void api.logs
.getDir()
.then((result) => setLogDir(result.dir))
.catch(() => {});
+ } catch {
+ // Native API not available (web-only mode)
+ }
}, []);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| const api = ensureNativeApi(); | |
| void api.logs | |
| .getDir() | |
| .then((result) => setLogDir(result.dir)) | |
| .catch(() => {}); | |
| }, []); | |
| useEffect(() => { | |
| try { | |
| const api = ensureNativeApi(); | |
| void api.logs | |
| .getDir() | |
| .then((result) => setLogDir(result.dir)) | |
| .catch(() => {}); | |
| } catch { | |
| // Native API not available (web-only mode) | |
| } | |
| }, []); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/routes/_chat.settings.tsx` around lines 342 - 348, The current
useEffect calls ensureNativeApi() synchronously which can throw when the native
API is unavailable; wrap the ensureNativeApi() call in a try-catch inside the
useEffect (around the call to ensureNativeApi and subsequent api.logs.getDir()
promise) so any synchronous exception is caught and ignored or handled, and only
call api.logs.getDir() when ensureNativeApi() succeeds; update the effect that
references ensureNativeApi(), api.logs.getDir(), and setLogDir accordingly.
Summary
This PR adds three user-facing features to improve debugging, error visibility, and personalization in T3 Code.
1. Log Viewer in Settings
A new Logs section in Settings that works in both the web app and desktop (Electron) modes:
server.log,desktop-main.log)key=valueformat:text-zinc-500)Info(blue),Warning(amber),Error(red),Debug(dim),Fatal(bold red)Architecture:
logs.getDir,logs.list,logs.read) with path traversal protectiondesktop:log-dir,desktop:log-list,desktop:log-read,desktop:log-open-dir) for ElectronDesktopBridgeandNativeApicontract updates2. Expandable Work Log Entries
Work log entries in the chat timeline (error messages, tool outputs, etc.) that were previously truncated with CSS
truncateand only visible via hover tooltip are now click-to-expand:cursor-pointervisual cueitems-startso the icon stays at the top when text wraps"Provider turn interrupt failed - Error: Provider validation failed in ProviderService.interruptTurn: Cannot recover thread '36649b16-5fab-45...'"which are now fully readable3. Custom Accent Color Presets
Users can now save custom accent colors as reusable presets:
window.prompt) where users type a name and hit Save or press EntercustomAccentPresetsinAppSettingsSchema4. Upstream Sync
Merged upstream
pingdotgg/t3codechangee6d9a271(fix GitHub bug report issue template for screenshots).Files Changed
packages/contracts/src/ipc.tsDesktopBridge:getLogDir,listLogFiles,readLogFile,openLogDir.NativeApi:logs.getDir/list/readpackages/contracts/src/ws.tslogs.getDir,logs.list,logs.readwith request body schemasapps/desktop/src/main.tsapps/desktop/src/preload.tsapps/server/src/wsServer.tslogs.getDir,logs.list,logs.readwith path traversal guardapps/web/src/wsNativeApi.tslogs.getDir/list/readWS transport callsapps/web/src/routes/_chat.settings.tsxapps/web/src/appSettings.tscustomAccentPresetsarray in settings schemaapps/web/src/components/chat/MessagesTimeline.tsxSimpleWorkEntryRowTest plan
bun typecheck— 7/7 packages passbun run fmt:check— cleanSummary by CodeRabbit
New Features
Documentation