From ef493a6a90fc2f34ca43868e514d381a16b91f5e Mon Sep 17 00:00:00 2001 From: prkl78 Date: Sun, 13 Sep 2026 12:24:00 +0300 Subject: [PATCH] Add a Compact transcript setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings → General → Compact transcript keeps a work group folded behind the summary line that already titles it, instead of opening it while the turn runs. A busy turn then reads as one row rather than a scrolling wall of steps. The auto-open was the only thing removed: clicking the line still opens the group, an override still sticks, and a step waiting on an approval still opens itself so nothing is approved unseen. A lone call with no headline never formed a group, so it is unaffected. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + src/hooks/useTranscriptCompact.ts | 19 +++++++++++++++++ src/lib/appearance.test.ts | 23 ++++++++++++++++++++ src/lib/appearance.ts | 21 ++++++++++++++++++ src/surfaces/AgentTranscript.test.ts | 32 +++++++++++++++++++++++++++- src/surfaces/AgentTranscript.tsx | 6 +++++- src/surfaces/SettingsView.tsx | 19 +++++++++++++++++ 7 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 src/hooks/useTranscriptCompact.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c1805384..e9670d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Settings → General → Compact transcript** keeps the work the agent is doing folded behind the summary line that already titles it, instead of opening the group while the turn runs. The line still shimmers, names the phase, and counts the steps, so a busy turn reads as one row rather than a scrolling wall. Clicking it opens the steps as before, and a call waiting on your approval opens itself regardless. - Subagents now get a row each in the transcript, sitting under the agent's own work with an animated mascot, a shimmering name while the run is live, and a count of the steps it has taken. Clicking a row opens the subagent's trail inline, grouped into phases the same way the main transcript groups work, so a long run reads as what it said and the calls that followed rather than one flat dump. The rows keep their place while the work above them folds and re-folds. Claude and Codex both report their subagents' work; Codex spawns are named from their brief, stay running until the agent itself reports otherwise, and one spawned agent no longer shows up as several rows. - GitLab's **Needs attention** Inbox view now uses pending GitLab To-Dos to include assignments, mentions, and review requests from every accessible repository. Remote-only items support details, discussions, comments, and merge-request diffs without requiring a local checkout. diff --git a/src/hooks/useTranscriptCompact.ts b/src/hooks/useTranscriptCompact.ts new file mode 100644 index 00000000..dac541c1 --- /dev/null +++ b/src/hooks/useTranscriptCompact.ts @@ -0,0 +1,19 @@ +import { useEffect, useState } from "react"; +import { + loadTranscriptCompact, + TRANSCRIPT_COMPACT_CHANGE_EVENT, +} from "../lib/appearance"; + +/** Subscribes to compact-transcript changes triggered by saveTranscriptCompact(). */ +export function useTranscriptCompact(): boolean { + const [compact, setCompact] = useState(loadTranscriptCompact); + useEffect(() => { + const onChange = (event: Event) => { + setCompact((event as CustomEvent).detail === true); + }; + window.addEventListener(TRANSCRIPT_COMPACT_CHANGE_EVENT, onChange); + return () => + window.removeEventListener(TRANSCRIPT_COMPACT_CHANGE_EVENT, onChange); + }, []); + return compact; +} diff --git a/src/lib/appearance.test.ts b/src/lib/appearance.test.ts index dfca0c23..1f6c0b6d 100644 --- a/src/lib/appearance.test.ts +++ b/src/lib/appearance.test.ts @@ -14,6 +14,9 @@ import { loadTranscriptAnchor, saveTranscriptAnchor, TRANSCRIPT_ANCHOR_DEFAULT, + loadTranscriptCompact, + saveTranscriptCompact, + TRANSCRIPT_COMPACT_DEFAULT, loadThemePreference, saveThemePreference, resolveColorScheme, @@ -23,6 +26,7 @@ import { const KEY = "monocode.transcriptLayout"; const SCHEME_KEY = "monocode.colorScheme"; const ANCHOR_KEY = "monocode.transcriptAnchor"; +const COMPACT_KEY = "monocode.transcriptCompact"; const CHAT_BACKGROUND_PATH_KEY = "monocode.chatBackgroundPath"; const CHAT_BACKGROUND_OPACITY_KEY = "monocode.chatBackgroundOpacity"; const CHAT_BACKGROUND_SCOPE_KEY = "monocode.chatBackgroundScope"; @@ -95,6 +99,25 @@ describe("transcript prompt-to-top setting", () => { }); }); +describe("compact transcript setting", () => { + beforeEach(mockLocalStorage); + afterEach(() => { + localStorage.removeItem(COMPACT_KEY); + }); + + it("defaults to off", () => { + expect(TRANSCRIPT_COMPACT_DEFAULT).toBe(false); + expect(loadTranscriptCompact()).toBe(false); + }); + + it("persists across loads", () => { + saveTranscriptCompact(true); + expect(loadTranscriptCompact()).toBe(true); + saveTranscriptCompact(false); + expect(loadTranscriptCompact()).toBe(false); + }); +}); + describe("chat background setting", () => { beforeEach(mockLocalStorage); afterEach(() => { diff --git a/src/lib/appearance.ts b/src/lib/appearance.ts index eb6dfaf9..efb7883a 100644 --- a/src/lib/appearance.ts +++ b/src/lib/appearance.ts @@ -13,6 +13,7 @@ const SIDEBAR_TAB_ORDER_KEY = "monocode.sidebarTabOrder"; const PROJECT_RAIL_WIDTH_KEY = "monocode.projectRailWidth"; const TRANSCRIPT_LAYOUT_KEY = "monocode.transcriptLayout"; const TRANSCRIPT_ANCHOR_KEY = "monocode.transcriptAnchor"; +const TRANSCRIPT_COMPACT_KEY = "monocode.transcriptCompact"; const CHAT_BACKGROUND_PATH_KEY = "monocode.chatBackgroundPath"; const CHAT_BACKGROUND_OPACITY_KEY = "monocode.chatBackgroundOpacity"; const CHAT_BACKGROUND_SCOPE_KEY = "monocode.chatBackgroundScope"; @@ -40,9 +41,15 @@ export const CHANGES_VIEW_DEFAULT: ChangesView = "list"; export const TRANSCRIPT_ANCHOR_DEFAULT = true; +export const TRANSCRIPT_COMPACT_DEFAULT = false; + /** Fired on `window` whenever prompt-to-top anchoring flips (detail: boolean). */ export const TRANSCRIPT_ANCHOR_CHANGE_EVENT = "monocode:transcriptanchorchange"; +/** Fired on `window` whenever compact transcript flips (detail: boolean). */ +export const TRANSCRIPT_COMPACT_CHANGE_EVENT = + "monocode:transcriptcompactchange"; + /** Fired on `window` whenever the transcript layout flips (detail: TranscriptLayout). */ export const TRANSCRIPT_LAYOUT_CHANGE_EVENT = "monocode:transcriptlayoutchange"; @@ -544,3 +551,17 @@ export function saveTranscriptAnchor(value: boolean) { }), ); } + +export function loadTranscriptCompact(): boolean { + return readFlag(TRANSCRIPT_COMPACT_KEY) ?? TRANSCRIPT_COMPACT_DEFAULT; +} + +export function saveTranscriptCompact(value: boolean) { + writeFlag(TRANSCRIPT_COMPACT_KEY, value); + if (typeof window === "undefined") return; + window.dispatchEvent( + new CustomEvent(TRANSCRIPT_COMPACT_CHANGE_EVENT, { + detail: value, + }), + ); +} diff --git a/src/surfaces/AgentTranscript.test.ts b/src/surfaces/AgentTranscript.test.ts index 42a2970a..2d7936cd 100644 --- a/src/surfaces/AgentTranscript.test.ts +++ b/src/surfaces/AgentTranscript.test.ts @@ -1,6 +1,6 @@ import { createElement, type ReactNode } from "react"; import { renderToStaticMarkup } from "react-dom/server"; -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { Block } from "../lib/session"; import { AgentTranscript } from "./AgentTranscript"; @@ -312,3 +312,33 @@ describe("AgentTranscript collapsed work", () => { ); }); }); + +describe("AgentTranscript compact transcript", () => { + beforeEach(() => { + Object.defineProperty(globalThis, "localStorage", { + value: { + getItem: (key: string) => + key === "monocode.transcriptCompact" ? "1" : null, + }, + configurable: true, + }); + }); + afterEach(() => { + Reflect.deleteProperty(globalThis, "localStorage"); + }); + + it("folds live work behind the line that titles it", () => { + const markup = render([tool("one"), tool("two"), tool("three")], true); + expect(markup).toContain("Running 3 commands"); + expect(markup.includes("hidden-detail-")).toBe(false); + }); + + it("still opens a group holding a call that waits on an approval", () => { + const markup = render( + [tool("one"), tool("two"), tool("approval", { requestId: 1 })], + true, + ); + expect(markup).toContain("hidden-detail-approval"); + expect(markup).toContain("hidden-detail-one"); + }); +}); diff --git a/src/surfaces/AgentTranscript.tsx b/src/surfaces/AgentTranscript.tsx index d5381d2a..f5971279 100644 --- a/src/surfaces/AgentTranscript.tsx +++ b/src/surfaces/AgentTranscript.tsx @@ -64,6 +64,7 @@ import { HarnessIcon } from "../chrome/HarnessIcon"; import { useLockOverscroll } from "../hooks/useLockOverscroll"; import { useTranscriptLayout } from "../hooks/useTranscriptLayout"; import { useTranscriptAnchor } from "../hooks/useTranscriptAnchor"; +import { useTranscriptCompact } from "../hooks/useTranscriptCompact"; import { useTranscriptSelection } from "../hooks/useTranscriptSelection"; import type { TranscriptLayout } from "../lib/appearance"; import { AgentMarkdown } from "./AgentMarkdown"; @@ -1391,7 +1392,10 @@ function ActivityPhaseGroup({ }) { const [override, setOverride] = useState(null); const waiting = phase.steps.some(needsApproval); - const open = waiting || (override ?? active); + // Compact drops the auto-open: the group stays the one line that already + // titles it. A click, or a step waiting on you, still opens it. + const compact = useTranscriptCompact(); + const open = waiting || (override ?? (active && !compact)); const [liveScroller, setLiveScroller] = useState(null); useLivePhaseScroll(liveScroller, active && open, phase.steps); const title = activityPhaseTitle(phase, active); diff --git a/src/surfaces/SettingsView.tsx b/src/surfaces/SettingsView.tsx index fb7d47a4..cb569d92 100644 --- a/src/surfaces/SettingsView.tsx +++ b/src/surfaces/SettingsView.tsx @@ -54,6 +54,7 @@ import { loadThemeSaturation, loadTranscriptLayout, loadTranscriptAnchor, + loadTranscriptCompact, saveBodyGlass, saveChatBackgroundOpacity, saveChatBackgroundPath, @@ -65,6 +66,7 @@ import { saveThemeSaturation, saveTranscriptLayout, saveTranscriptAnchor, + saveTranscriptCompact, TRANSCRIPT_ANCHOR_CHANGE_EVENT, SIDEBAR_BLUR_DEFAULT, SIDEBAR_BLUR_MAX, @@ -354,6 +356,8 @@ function GeneralPage({ useState(loadTranscriptLayout); const [transcriptAnchor, setTranscriptAnchor] = useState(loadTranscriptAnchor); + const [transcriptCompact, setTranscriptCompact] = + useState(loadTranscriptCompact); const [diffViewer, setDiffViewer] = useState(loadDiffViewer); const [followUpBehavior, setFollowUpBehavior] = useState(loadFollowUpBehavior); @@ -408,6 +412,11 @@ function GeneralPage({ setTranscriptAnchor(next); }; + const onTranscriptCompact = (next: boolean) => { + saveTranscriptCompact(next); + setTranscriptCompact(next); + }; + const onDiffViewer = (next: DiffViewer) => { saveDiffViewer(next); setDiffViewer(next); @@ -514,6 +523,16 @@ function GeneralPage({ onChange={onTranscriptAnchor} /> + + +