feat: show script output in launcher and notifications after every run#322
Conversation
Scripts now keep their last-line output preview after they finish, on both
the success and failure paths, so users can always see how a script ended
without having to open the run detail view in time.
- Capture Run.tailOutput at finalize from the in-memory OutputBuffer; keep
the buffer alive past finalize so RunView can show the full output
post-mortem and dismiss explicitly frees it via runs_dismiss.
- Succeeded shell-scripts persist as "Done · {output}" rows in the Scripts
section until dismissed (mirroring the kept-agents pattern); previously
they vanished silently.
- Failed-run rows and notifications now lead with the captured output tail
instead of just the run label or raw error code.
- New "Script finished" notification carries the tail preview and opens
RunView with the run pre-selected; agent runs still notify only on
failure.
There was a problem hiding this comment.
Code Review
This pull request implements persistent tracking for shell script results, enabling users to view script status and output in the launcher until they are explicitly dismissed. Key updates include the capture and persistence of a 'tail output' preview for use in UI subtitles and notifications, along with logic to keep output buffers available post-mortem. Review feedback identifies a potential memory leak when runs are evicted from capped frontend slices, suggests optimizing backend tail-capture to avoid cloning large buffers, and provides a correction for the truncate function to properly handle multi-unit Unicode characters.
| ].slice(0, UNACK_FAILED_CAP); | ||
| } | ||
|
|
||
| if (run.status === 'succeeded' && run.kind === 'shell-script') { | ||
| // Scripts persist after success so the user can read the output. | ||
| // Dedupe by subjectId when present so re-running the same script row | ||
| // collapses into one entry; anonymous (no subjectId) runs dedupe by id. | ||
| const filtered = run.subjectId | ||
| ? this.unacknowledgedScriptResults.filter((r) => r.subjectId !== run.subjectId) | ||
| : this.unacknowledgedScriptResults.filter((r) => r.id !== run.id); | ||
| this.unacknowledgedScriptResults = [run, ...filtered].slice(0, UNACK_FAILED_CAP); |
There was a problem hiding this comment.
| let snapshot = buffer.snapshot(id); | ||
| let tail = format_tail_output(&snapshot); |
There was a problem hiding this comment.
buffer.snapshot(id) clones the entire output buffer (up to 10,000 lines) into a Vec<String> just to extract the last non-empty line. This is inefficient for runs with large output. Consider adding a method to OutputBuffer that retrieves only the tail or the last non-empty line directly from the VecDeque without cloning the entire collection.
| function truncate(text: string, max = NOTIFICATION_BODY_MAX): string { | ||
| if (text.length <= max) return text; | ||
| return text.slice(0, max - 1) + '…'; | ||
| } |
There was a problem hiding this comment.
The truncate function uses String.prototype.slice and String.prototype.length, which operate on UTF-16 code units. This can break multi-unit characters (like emojis) if they fall on the truncation boundary, resulting in malformed strings with dangling surrogates.
| function truncate(text: string, max = NOTIFICATION_BODY_MAX): string { | |
| if (text.length <= max) return text; | |
| return text.slice(0, max - 1) + '…'; | |
| } | |
| function truncate(text: string, max = NOTIFICATION_BODY_MAX): string { | |
| const chars = Array.from(text); | |
| if (chars.length <= max) return text; | |
| return chars.slice(0, max - 1).join('') + '…'; | |
| } |
feat: show script output in launcher and notifications after every run
Scripts now keep their last-line output preview after they finish, on both
the success and failure paths, so users can always see how a script ended
without having to open the run detail view in time.
the buffer alive past finalize so RunView can show the full output
post-mortem and dismiss explicitly frees it via runs_dismiss.
section until dismissed (mirroring the kept-agents pattern); previously
they vanished silently.
instead of just the run label or raw error code.
RunView with the run pre-selected; agent runs still notify only on
failure.