fix: stop artifact walk entering OS app-data dirs; context bar off by default

The artifacts scan used rglob and filtered after descending, so a home directory
workspace walked into ~/Library and triggered the macOS App Data consent prompt on
every turn. Walk with pruning instead, and skip Library / AppData in search too.

The composer chip now shows the session total by default, with the context window
bar behind a Settings toggle.
This commit is contained in:
Rohit C Prasad
2026-07-30 13:10:30 -07:00
parent 11d9f72e51
commit 25dc283d9b
10 changed files with 236 additions and 37 deletions
+4
View File
@@ -838,6 +838,10 @@ export async function mockApi(page: import("@playwright/test").Page) {
if (p.endsWith("/v1/health")) return json(HEALTH);
if (p.endsWith("/v1/settings")) return json(SETTINGS);
if (p.endsWith("/v1/settings/context-bar") && m === "POST") {
Object.assign(SETTINGS, req.postDataJSON());
return json({ ok: true, context_bar: SETTINGS.context_bar });
}
if (p.endsWith("/v1/settings/pdf") && m === "POST") {
Object.assign(SETTINGS, req.postDataJSON());
return json({
+35 -2
View File
@@ -20,7 +20,8 @@ test("usage chip appears after a turn and opens the breakdown popover", async ({
timeout: 10_000,
});
// Chip shows the session total (1k + 200 + 8k + 800 = 10k).
// Default: no bar (owner ask 2026-07-30) — the chip states the session total
// (1k + 200 + 8k + 800 = 10k). The bar is opt-in via Settings.
const chip = page.getByTestId("usage-chip");
await expect(chip).toContainText("10k");
@@ -58,9 +59,41 @@ test("usage resets on a new session", async ({ page }) => {
const box = page.getByPlaceholder(/Ask the coworker/);
await box.fill("hello");
await box.press("Enter");
await expect(page.getByTestId("usage-chip")).toContainText("10k", { timeout: 10_000 });
await expect(page.getByTestId("usage-chip")).toBeVisible({ timeout: 10_000 });
// " New session" wipes the transcript — and the usage accumulation with it.
await page.getByRole("button", { name: /New session/ }).first().click();
await expect(page.getByTestId("usage-chip")).toHaveCount(0);
});
test("Settings toggle turns the context bar on; default is the session total", async ({ page }) => {
await page.goto("/");
await page.getByText("Draft the launch note").first().click();
const box = page.getByPlaceholder(/Ask the coworker/);
await box.fill("hello");
await box.press("Enter");
const chip = page.getByTestId("usage-chip");
await expect(chip).toContainText("10k", { timeout: 10_000 }); // default: total, no bar
// Turn the bar ON in Settings -> General.
await page.getByTestId("account-row").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await expect(page.getByTestId("context-bar-toggle")).not.toBeChecked();
const [req] = await Promise.all([
page.waitForRequest(
(r) => r.url().endsWith("/v1/settings/context-bar") && r.method() === "POST",
),
page.getByTestId("context-bar-toggle").check(),
]);
expect(req.postDataJSON()).toEqual({ context_bar: true });
// Reload so the app re-reads settings: the chip is now the fill bar, not a number.
await page.goto("/");
await page.getByText("Draft the launch note").first().click();
await page.getByPlaceholder(/Ask the coworker/).fill("hello");
await page.getByPlaceholder(/Ask the coworker/).press("Enter");
const bar = page.getByTestId("usage-chip");
await expect(bar).toBeVisible({ timeout: 10_000 });
await expect(bar).not.toContainText("10k");
await expect(bar).toHaveAttribute("title", /Context window 5% full/);
});
+5
View File
@@ -165,6 +165,9 @@ export function App() {
// {full model id → context window in tokens} from the curated matrix (verified only);
// drives the composer usage chip's context-fill meter.
const [modelContextWindows, setModelContextWindows] = useState<Record<string, number>>({});
// Settings: show the composer's context-window fill bar. OFF by default (owner ask),
// so an older backend without the field also shows the session total.
const [contextBar, setContextBar] = useState(false);
// Per-session token usage (OPE-42): rebuilt from the transcript on session load,
// accumulated live from assistant_message events, reset with the transcript.
const [usage, setUsage] = useState<SessionUsage>(emptyUsage());
@@ -506,6 +509,7 @@ export function App() {
setModels(s.models || []);
setModelLabels(s.model_labels || {});
setModelContextWindows(s.model_context_windows || {});
setContextBar(s.context_bar === true);
setModelReady(s.model_ready);
if (s.surfaces) setSurfaces(s.surfaces);
})
@@ -1587,6 +1591,7 @@ export function App() {
resetKey={sessionId}
usage={usage}
contextWindow={modelContextWindows[model]}
contextBar={contextBar}
placeholder={
agent === "code"
? "Ask the coder to build, fix, or explain… (drop or paste files)"
+15
View File
@@ -692,6 +692,9 @@ export interface ModelSettings {
nav_layout?: "flat" | "grouped";
// Sidebar: sessions shown per group before "Show more" (default 5, 150).
sessions_peek?: number;
// Composer: show the context-window fill bar (default FALSE; absent → the chip shows
// the session total). The usage popover keeps both numbers regardless.
context_bar?: boolean;
// Curated-matrix display names ({full id → "GLM-5.2 · via Together"}); custom models absent.
model_labels?: Record<string, string>;
// {full id → context window in tokens}, verified matrix entries only — drives the
@@ -758,6 +761,18 @@ export async function inspectPdf(
return res.json();
}
/** Persist whether the composer shows the context-window fill bar. */
export async function setContextBar(
shown: boolean,
): Promise<{ ok: boolean; context_bar?: boolean; error?: string }> {
const res = await fetch(`${httpBase()}/v1/settings/context-bar`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ context_bar: shown }),
});
return res.json();
}
/** Persist how many sessions a sidebar group shows before "Show more". */
export async function setSessionsPeek(
n: number,
+21 -10
View File
@@ -84,6 +84,8 @@ interface Props {
// Context-window size (tokens) of the ACTIVE model, from the curated matrix;
// undefined hides the fill meter (unverified/custom models) but keeps the counts.
contextWindow?: number;
// Settings toggle (default off): true shows the fill bar instead of the session total.
contextBar?: boolean;
}
export function Composer(props: Props) {
@@ -469,13 +471,14 @@ export function Composer(props: Props) {
<span className="ml-auto" />
{/* token usage (OPE-42) a quiet meter+count chip; hidden until the server
reports usage. Fill = context-window occupancy (bounded), count = session
consumption (unbounded, so never a fill). */}
{/* token usage (OPE-42) a quiet chip; hidden until the server reports usage.
Shows the context-window fill bar alone (the session total lives in the
popover), or the session total when there's no window / the bar is off. */}
{!dictation?.recording && props.usage && totalTokens(props.usage) > 0 && (
<UsageChip
usage={props.usage}
contextWindow={props.contextWindow}
contextBar={props.contextBar}
model={props.model}
modelLabels={props.modelLabels}
/>
@@ -572,11 +575,13 @@ export function Composer(props: Props) {
function UsageChip({
usage,
contextWindow,
contextBar,
model,
modelLabels,
}: {
usage: SessionUsage;
contextWindow?: number;
contextBar?: boolean;
model: string;
modelLabels?: Record<string, string>;
}) {
@@ -585,6 +590,8 @@ function UsageChip({
const pct = contextWindow
? Math.min(100, Math.round((usage.context / contextWindow) * 100))
: null;
// Settings can hide the bar; without a known window there is nothing to fill either.
const showBar = pct !== null && contextBar === true;
const labelFor = (id: string) =>
id === "unknown" ? "Unknown model" : modelLabels?.[id] || shortModel(id);
// One field per line, session-summed (owner ask 2026-07-28). Values are cumulative
@@ -605,21 +612,25 @@ function UsageChip({
aria-expanded={open}
aria-label="Token usage"
title={
pct !== null
? `Token usage — ${pct}% of the context window used`
: "Token usage this session"
showBar
? `Context window ${pct}% full · ${formatTokens(total)} tokens this session`
: `Token usage this session: ${formatTokens(total)}`
}
data-testid="usage-chip"
>
{pct !== null && (
<span className="w-7 h-1 rounded-full bg-line overflow-hidden" aria-hidden="true">
{/* The bar is the context-window fill; pairing it with the session TOTAL read as
"total is N% of the window", which it never was. Bar alone when we have a
window, the session total only when we don't (so the chip is never empty). */}
{showBar ? (
<span className="w-12 h-1.5 rounded-full bg-line overflow-hidden" aria-hidden="true">
<span
className="block h-full bg-accent transition-all"
style={{ width: `${Math.max(pct, 4)}%` }}
style={{ width: `${Math.max(pct as number, 4)}%` }}
/>
</span>
) : (
<span className="tabular-nums">{formatTokens(total)}</span>
)}
<span className="tabular-nums">{formatTokens(total)}</span>
</button>
{open && (
<>
@@ -3,6 +3,7 @@ import {
getSettings,
getTrustedWorkspaces,
setCompactionSettings,
setContextBar,
setOnboarded,
setPdfSettings,
setScratchBase,
@@ -428,6 +429,8 @@ function AppearanceSection() {
<SidebarCard />
<ContextBarCard />
<FilesCard />
<TrustedWorkspacesCard />
@@ -790,6 +793,48 @@ function CompactionCard() {
);
}
// -- Composer: context-window bar (owner ask 2026-07-30) ------------------------
// The chip's bar is context-window occupancy; the session total (unbounded) lives in
// the popover. Some people would rather not watch a meter at all, hence the toggle.
function ContextBarCard() {
const [shown, setShown] = useState<boolean | null>(null);
useEffect(() => {
getSettings()
.then((s) => setShown(s.context_bar === true))
.catch(() => setShown(false));
}, []);
const save = async (next: boolean) => {
setShown(next);
await setContextBar(next);
};
if (shown === null) return null;
return (
<div className={CARD + " p-4 mb-4"} data-testid="context-bar-card">
<div className={FIELD_LABEL}>Composer</div>
<label className="flex items-start gap-3 py-2">
<input
type="checkbox"
className="mt-0.5"
data-testid="context-bar-toggle"
checked={shown}
onChange={(e) => save(e.target.checked)}
/>
<span>
<span className="block text-[13px] text-ink">Show the context window bar</span>
<span className="block text-[12px] text-muted">
A small meter showing how full the model&rsquo;s context window is. Turn it off
to show this session&rsquo;s token total instead; either way the full breakdown
is one click away.
</span>
</span>
</label>
</div>
);
}
function SidebarCard() {
const [peek, setPeek] = useState<number | null>(null);