)}
+ {/* Agent name pill for toast entries (from data.project) */}
+ {(() => {
+ if (log.level !== 'toast') return null;
+ const data = log.data as { project?: string } | undefined;
+ const project = data?.project;
+ if (!project) return null;
+ return (
+
+
+ {project}
+
+ );
+ })()}
+ {/* Agent name pill for autorun entries (from context) */}
+ {log.level === 'autorun' && log.context && (
+
+
+ {log.context}
+
+ )}
{log.message}
diff --git a/src/renderer/hooks/git/useFileTreeManagement.ts b/src/renderer/hooks/git/useFileTreeManagement.ts
index 2dc22856..1de99933 100644
--- a/src/renderer/hooks/git/useFileTreeManagement.ts
+++ b/src/renderer/hooks/git/useFileTreeManagement.ts
@@ -233,8 +233,10 @@ export function useFileTreeManagement(
const session = sessions.find(s => s.id === activeSessionId);
if (!session) return;
- // Only load if file tree is empty and not already loading
- if ((!session.fileTree || session.fileTree.length === 0) && !session.fileTreeLoading) {
+ // Only load if file tree is empty, not already loading, and hasn't been loaded yet
+ // fileTreeStats is set after successful load, so we use it to detect "loaded but empty"
+ const hasLoadedOnce = session.fileTreeStats !== undefined || session.fileTreeError !== undefined;
+ if ((!session.fileTree || session.fileTree.length === 0) && !session.fileTreeLoading && !hasLoadedOnce) {
// Check if we're in a retry backoff period
if (session.fileTreeRetryAt && Date.now() < session.fileTreeRetryAt) {
// Schedule retry when backoff expires (if not already scheduled)
diff --git a/src/shared/synopsis.ts b/src/shared/synopsis.ts
index 1b90bfcd..7d5860fe 100644
--- a/src/shared/synopsis.ts
+++ b/src/shared/synopsis.ts
@@ -4,13 +4,22 @@
*
* Functions:
* - parseSynopsis: Parse AI-generated synopsis responses into structured format
+ * - isNothingToReport: Check if response indicates no meaningful work was done
*/
import { stripAnsiCodes } from './stringUtils';
+/**
+ * Sentinel token that AI agents should return when there's nothing meaningful to report.
+ * When detected, callers should skip creating a history entry.
+ */
+export const NOTHING_TO_REPORT = 'NOTHING_TO_REPORT';
+
export interface ParsedSynopsis {
shortSummary: string;
fullSynopsis: string;
+ /** True if the AI indicated there was nothing meaningful to report */
+ nothingToReport: boolean;
}
/**
@@ -29,6 +38,22 @@ function isTemplatePlaceholder(text: string): boolean {
return placeholderPatterns.some(pattern => pattern.test(text.trim()));
}
+/**
+ * Check if a response indicates nothing meaningful to report.
+ * Looks for the NOTHING_TO_REPORT sentinel token anywhere in the response.
+ *
+ * @param response - Raw AI response string
+ * @returns True if the response contains NOTHING_TO_REPORT
+ */
+export function isNothingToReport(response: string): boolean {
+ const clean = stripAnsiCodes(response)
+ .replace(/─+/g, '')
+ .replace(/[│┌┐└┘├┤┬┴┼]/g, '')
+ .trim();
+
+ return clean.includes(NOTHING_TO_REPORT);
+}
+
/**
* Parse a synopsis response into short summary and full synopsis.
*
@@ -40,8 +65,11 @@ function isTemplatePlaceholder(text: string): boolean {
* Filters out template placeholders that models sometimes output literally
* (especially common with thinking/reasoning models).
*
+ * If the response contains NOTHING_TO_REPORT, returns nothingToReport: true
+ * and callers should skip creating a history entry.
+ *
* @param response - Raw AI response string (may contain ANSI codes, box drawing chars)
- * @returns Parsed synopsis with shortSummary and fullSynopsis
+ * @returns Parsed synopsis with shortSummary, fullSynopsis, and nothingToReport flag
*/
export function parseSynopsis(response: string): ParsedSynopsis {
// Clean up ANSI codes and box drawing characters
@@ -50,6 +78,15 @@ export function parseSynopsis(response: string): ParsedSynopsis {
.replace(/[│┌┐└┘├┤┬┴┼]/g, '')
.trim();
+ // Check for the sentinel token first
+ if (clean.includes(NOTHING_TO_REPORT)) {
+ return {
+ shortSummary: '',
+ fullSynopsis: '',
+ nothingToReport: true,
+ };
+ }
+
// Try to extract Summary and Details sections
const summaryMatch = clean.match(/\*\*Summary:\*\*\s*(.+?)(?=\*\*Details:\*\*|$)/is);
const detailsMatch = clean.match(/\*\*Details:\*\*\s*(.+?)$/is);
@@ -82,5 +119,5 @@ export function parseSynopsis(response: string): ParsedSynopsis {
// Full synopsis includes both parts
const fullSynopsis = details ? `${shortSummary}\n\n${details}` : shortSummary;
- return { shortSummary, fullSynopsis };
+ return { shortSummary, fullSynopsis, nothingToReport: false };
}