Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/design-surfaces.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
"about": "Which source paths make up each UI surface, and the design spec that surface follows (one spec per surface, in leo-workspace design/<surface>.md). scripts/ci/pr_design_spec_check.py warns, never fails, when a pull request changes a surface's paths and its description does not cite the spec. leo-workspace tools/prior_art.py reads this map to list a surface's open and recently merged PRs before work starts. A path is a file or a directory (everything under it). Tests and gallery files are left out: changing them changes no design. Keep paths current when files move; the check warns about a mapped path that no longer exists.",
"version": 1,
"surfaces": {
"context-viewer": {
"spec": "leo-workspace design/context-viewer.md",
"aliases": ["Context tab", "prompt viewer", "effective context"],
"paths": [
"webviews/src/agent-session/acpmux/ContextViewer.tsx",
"webviews/src/agent-session/acpmux/contextViewer.css",
"webviews/src/agent-session/acpmux/contextPane.ts"
]
},
"onboarding": {
"spec": "leo-workspace design/onboarding.md",
"aliases": ["onboarding", "bring your history", "memory import"],
Expand Down
25 changes: 25 additions & 0 deletions docs/cmux-next/context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Session context

Open a session's **…** menu and choose **Context**. The Context tab shows
the harness's reported context categories and the cmux additions captured when
the session was created. Expand a row to see its source text when available.

Token counts are labelled **measured**, **estimated**, or **unavailable**.
Harness category rows can include their tool, memory, and skill detail rows;
they are not added together. A reported total is shown separately from the
estimate for cmux-owned text. Provider-managed prompts and runtime hook output
may not be exposed by the harness.

Use **Next session** to replace cmux guidance for your account or the
current project. For a local Claude preset with a cmux-owned system prompt,
the **Part** menu also lets you edit that preset’s prompt independently.
Preset edits apply only to the selected preset. Project settings take precedence over account settings. An
empty saved override disables the selected cmux addition at that scope; **Reset** restores
inheritance. These settings apply when a new session is created. The effective
context of an existing session remains unchanged.

Remote clients without permission to edit settings show the available context
report without an editor.

MCP instructions, tool contracts, skill descriptions, and provider-owned prompts
are read-only. Text the harness does not expose remains unavailable.
4 changes: 4 additions & 0 deletions release-notes/next/highlights/session-context.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
title: See and customize your session context
category: new

Open Context from a session’s menu to inspect its prompt sources and token counts in a new tab. Edit cmux guidance for yourself or a project, then start a new session to use it.
132 changes: 131 additions & 1 deletion webviews/src/agent-session/acpmux/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,9 @@ import { useWorkflowTranscript } from "./summary/useWorkflowTranscript";
import { noteReaderScroll, releaseReaderScroll, useHeldWhileScrolling } from "./summary/heldWhileScrolling";
import type { WorkflowAgent, WorkflowSummary } from "./summary/workflow";
import { WorkflowSideTab } from "./summary/WorkflowSideTab";
import { ContextViewer, type ContextPart as ContextViewerPart, type ContextOverrideScope } from "./ContextViewer";
import { isContextPane, openContextPane } from "./contextPane";
import { contextInventory } from "./contextInventory";
import {
showSubagent as showSubagentIn,
revealedSubagent,
Expand Down Expand Up @@ -6487,6 +6490,7 @@ function AcpmuxPane({ initialChat }: { initialChat?: InitialChat }) {
/// This page is the chat's Background tasks tab in the right column (a `subagent:` reveal of the
/// reserved pane id, backgroundTasks/backgroundPane.ts): it shows the panel alone, filling the tab.
const [ownBackgroundPane, setOwnBackgroundPane] = useState(false);
const [ownContextPane, setOwnContextPane] = useState(false);
if (ownBackgroundPane && !backgroundPanel && snapshot.sessionId)
setBackgroundPanel({ sessionId: snapshot.sessionId, expanded: false, dismissed: new Set<string>() });
const backgroundOpen = backgroundPanel !== undefined;
Expand Down Expand Up @@ -6662,6 +6666,10 @@ function AcpmuxPane({ initialChat }: { initialChat?: InitialChat }) {
setOwnBackgroundPane(true);
return;
}
if (isContextPane(agent)) {
setOwnContextPane(true);
return;
}
subagentPane.current = true;
setOwnSubagentPane(true);
setRequestedSubagent(agent);
Expand Down Expand Up @@ -7597,6 +7605,16 @@ function AcpmuxPane({ initialChat }: { initialChat?: InitialChat }) {
[snapshot.sessions, shownSessionId],
);
const [workflowSideTabId, setWorkflowSideTabId] = useState<string>();
const [contextViewerOpen, setContextViewerOpen] = useState(false);
const [contextViewerParts, setContextViewerParts] = useState<ContextViewerPart[]>([]);
const [contextViewerTotalTokens, setContextViewerTotalTokens] = useState<number>();
const [contextViewerMaxTokens, setContextViewerMaxTokens] = useState<number>();
const [contextViewerCmuxTokens, setContextViewerCmuxTokens] = useState<number>();
const [contextViewerSettings, setContextViewerSettings] = useState<ReturnType<typeof contextInventory>["settings"]>();
const [contextViewerLoading, setContextViewerLoading] = useState(false);
const [contextViewerError, setContextViewerError] = useState<string>();
const [contextViewerSessionId, setContextViewerSessionId] = useState<string>();
const contextRequest = useRef(0);
const workflowSideTabOpener = useRef<HTMLElement | undefined>(undefined);
const workflowSideTabFocusOnClose = useRef(false);
const workflowSideTab = useMemo(
Expand All @@ -7608,6 +7626,84 @@ function AcpmuxPane({ initialChat }: { initialChat?: InitialChat }) {
workflowSideTabOpener.current = undefined;
setWorkflowSideTabId(undefined);
}, []);
const closeContextViewer = useCallback(() => {
contextRequest.current += 1;
setContextViewerOpen(false);
setContextViewerLoading(false);
setContextViewerError(undefined);
}, []);
const loadContextViewer = useCallback(async () => {
const sessionId = shownSessionId ?? snapshot.sessionId;
if (!sessionId) return;
const request = ++contextRequest.current;
setContextViewerSessionId(sessionId);
setContextViewerParts([]);
setContextViewerSettings(undefined);
setContextViewerTotalTokens(undefined);
setContextViewerMaxTokens(undefined);
setContextViewerCmuxTokens(undefined);
setContextViewerOpen(true);
setContextViewerLoading(true);
setContextViewerError(undefined);
try {
const client = directClient.current;
if (!client) throw new Error(t("error.nativeUnavailable"));
const inventory = contextInventory(await client.contextInventory(sessionId));
if (request !== contextRequest.current) return;
setContextViewerParts(inventory.parts);
setContextViewerTotalTokens(inventory.totalTokens);
setContextViewerMaxTokens(inventory.maxTokens);
setContextViewerCmuxTokens(inventory.cmuxTokens);
setContextViewerSettings(inventory.settings);
} catch (error) {
if (request !== contextRequest.current) return;
setContextViewerError(error instanceof Error ? error.message : t("error.nativeUnavailable"));
} finally {
if (request === contextRequest.current) setContextViewerLoading(false);
}
}, [shownSessionId, snapshot.sessionId]);
const saveContextOverride = useCallback(
async (scope: ContextOverrideScope, text: string, partId?: string) => {
const sessionId = shownSessionId ?? snapshot.sessionId;
if (!sessionId) throw new Error(t("permission.error.selectSession"));
const client = directClient.current;
if (!client) throw new Error(t("error.nativeUnavailable"));
const request = contextRequest.current;
const editablePartId = partId === "cmux-system-prompt" ? partId : undefined;
const next = contextInventory(await client.setContextOverride(sessionId, scope, text, editablePartId));
if (request === contextRequest.current) setContextViewerSettings(next.settings);
},
[shownSessionId, snapshot.sessionId],
);
const resetContextOverride = useCallback(
async (scope: ContextOverrideScope, partId?: string) => {
const sessionId = shownSessionId ?? snapshot.sessionId;
if (!sessionId) throw new Error(t("permission.error.selectSession"));
const client = directClient.current;
if (!client) throw new Error(t("error.nativeUnavailable"));
const request = contextRequest.current;
const editablePartId = partId === "cmux-system-prompt" ? partId : undefined;
const next = contextInventory(await client.setContextOverride(sessionId, scope, null, editablePartId));
if (request === contextRequest.current) setContextViewerSettings(next.settings);
},
[shownSessionId, snapshot.sessionId],
);
useEffect(() => {
closeContextViewer();
}, [shownSessionId, closeContextViewer]);
useEffect(() => {
if (ownContextPane) void loadContextViewer();
return () => {
contextRequest.current += 1;
};
}, [ownContextPane, loadContextViewer]);
const openContextViewer = useCallback(() => {
if (ownContextPane) return void loadContextViewer();
const request = contextRequest.current;
void openContextPane(callNative, t("context.viewer.title")).then((opened) => {
if (!opened && request === contextRequest.current) void loadContextViewer();
});
}, [ownContextPane, loadContextViewer, t]);
// The Background tasks panel opens from the chat's ⋯ menu and from the running tray's header
// (SubagentTray.tsx `onOpenPanel`). A workflow's side tab would cover it (a full-height overlay):
// it closes. Already open, it stays as it is (expanded, cleared and drilled in alike).
Expand Down Expand Up @@ -12870,6 +12966,16 @@ function AcpmuxPane({ initialChat }: { initialChat?: InitialChat }) {
...replay,
...(instructions ? [instructions] : []),
...(projectFiles ? [projectFiles] : []),
...(snapshot.sessionId
? [
{
key: "context",
label: t("context.viewer.title"),
icon: "context.usage",
onSelect: openContextViewer,
},
]
: []),
{
key: "inspector",
label: t("inspector.title"),
Expand Down Expand Up @@ -14683,6 +14789,7 @@ function AcpmuxPane({ initialChat }: { initialChat?: InitialChat }) {
className="acpmux-shell"
data-tab-row={inTabRow ? "" : undefined}
data-background-pane={ownBackgroundPane ? "" : undefined}
data-context-pane={ownContextPane ? "" : undefined}
aria-label={composerSnapshot.summary?.title || t("header.agentChat")}
style={inTabRow ? tabRowStyle(tabRow) : undefined}
>
Expand Down Expand Up @@ -14947,7 +15054,7 @@ function AcpmuxPane({ initialChat }: { initialChat?: InitialChat }) {
/>
)}
/>
) : (
) : ownContextPane ? null : (
<>
<div
className={`acpmux-stage${inspectorOpen ? " acpmux-inspecting" : ""}`}
Expand Down Expand Up @@ -15342,6 +15449,29 @@ function AcpmuxPane({ initialChat }: { initialChat?: InitialChat }) {
/>
</SubagentClockContext.Provider>
)}
{contextViewerOpen &&
!harnessesOpen &&
!showNewTab &&
!replaying &&
!inspectorOpen &&
!artifact &&
!imageView && (
<ContextViewer
parts={contextViewerSessionId === shownSessionId ? contextViewerParts : []}
totalTokens={contextViewerSessionId === shownSessionId ? contextViewerTotalTokens : undefined}
maxTokens={contextViewerSessionId === shownSessionId ? contextViewerMaxTokens : undefined}
cmuxTokens={contextViewerSessionId === shownSessionId ? contextViewerCmuxTokens : undefined}
settings={contextViewerSessionId === shownSessionId ? contextViewerSettings : undefined}
harness={snapshot.summary?.harness}
sessionId={shownSessionId}
loading={contextViewerLoading || contextViewerSessionId !== shownSessionId}
onRefresh={loadContextViewer}
error={contextViewerSessionId === shownSessionId ? contextViewerError : undefined}
onClose={ownContextPane ? undefined : closeContextViewer}
onSaveOverride={saveContextOverride}
onResetOverride={resetContextOverride}
/>
)}
</section>
</ShortcutsContext.Provider>
);
Expand Down
Loading
Loading