Skip to content
Open
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
58 changes: 58 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,64 @@ header[data-tauri-drag-region] button {
border-bottom-left-radius: 8px;
}

/*
* Prose the fold holds is the agent talking while it works — process, not
* result. It stays readable at one notch quieter: muted ink, headings
* demoted to bold lines, and tighter spacing, so a mid-run note never poses
* as the answer it led to.
*/
.zen-fold-prose .agent-markdown > :not(:last-child) {
margin-block-end: 0.5rem;
}

.zen-fold-prose .agent-markdown p,
.zen-fold-prose .agent-markdown li {
color: color-mix(in srgb, var(--color-content) 65%, transparent);
}

.zen-fold-prose .agent-markdown p strong,
.zen-fold-prose .agent-markdown p em,
.zen-fold-prose .agent-markdown p [data-streamdown="strong"],
.zen-fold-prose .agent-markdown li strong,
.zen-fold-prose .agent-markdown li em,
.zen-fold-prose .agent-markdown li [data-streamdown="strong"] {
color: color-mix(in srgb, var(--color-content) 80%, transparent) !important;
}

/* A heading in a work note is a bold line, not a display size. */
.zen-fold-prose .agent-markdown [data-streamdown="heading-1"],
.zen-fold-prose .agent-markdown [data-streamdown="heading-2"],
.zen-fold-prose .agent-markdown [data-streamdown="heading-3"],
.zen-fold-prose .agent-markdown [data-streamdown="heading-4"],
.zen-fold-prose .agent-markdown [data-streamdown="heading-5"],
.zen-fold-prose .agent-markdown [data-streamdown="heading-6"] {
margin-top: 0.75rem;
margin-bottom: 0.25rem;
font-size: 14px;
font-weight: 600;
color: color-mix(in srgb, var(--color-content) 80%, transparent);
}

.zen-fold-prose .agent-markdown [data-streamdown="blockquote"],
.zen-fold-prose .agent-markdown .markdown-code-shell {
margin-block: 0.5rem;
}

.zen-fold-prose .agent-markdown [data-streamdown="code-block"],
.zen-fold-prose .agent-markdown [data-streamdown="table-wrapper"] {
border-color: color-mix(in srgb, var(--color-content) 8%, transparent);
background: color-mix(in srgb, var(--color-content) 4%, transparent);
}

.zen-fold-prose .agent-markdown [data-streamdown="table-header-cell"],
.zen-fold-prose .agent-markdown [data-streamdown="table-cell"] {
color: color-mix(in srgb, var(--color-content) 65%, transparent);
}

.zen-fold-prose .agent-markdown [data-streamdown="horizontal-rule"] {
margin-block: 0.75rem;
}

.zen-fold-item[data-fold-state="closed"] {
display: grid;
grid-template-rows: 0fr;
Expand Down
81 changes: 81 additions & 0 deletions src/surfaces/AgentTranscript.foldProse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// @vitest-environment happy-dom
import { act, createElement } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Block } from "../lib/session";
import { AgentTranscript } from "./AgentTranscript";

let container: HTMLDivElement;
let root: Root;

beforeEach(() => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.stubGlobal(
"ResizeObserver",
class {
observe() {}
disconnect() {}
},
);
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
});

afterEach(() => {
act(() => root.unmount());
container.remove();
vi.unstubAllGlobals();
});

function tool(id: string): Block {
return {
id,
role: "tool",
text: `Inspect ${id}`,
tool: { kind: "shell", status: "completed" },
};
}

describe("prose folded into the work trail", () => {
it("marks a mid-turn note as process, leaving the answer at full strength", () => {
const blocks: Block[] = [
{ id: "user", role: "user", text: "Keep me posted" },
tool("t1"),
{ id: "note", role: "assistant", text: "Trying the other config." },
tool("t2"),
{
id: "answer",
role: "assistant",
text: "The investigation is complete.",
},
];
act(() => root.render(createElement(AgentTranscript, { blocks })));

// The trail stays collapsed until asked for, so none of what it holds —
// the note included — is in the DOM yet.
expect(container.querySelector(".zen-fold-prose")).toBeNull();
const toggle = container.querySelector<HTMLButtonElement>(
'button[aria-label="Show the work"]',
)!;
expect(toggle).not.toBeNull();

act(() => toggle.click());

// Only the mid-turn note carries the marker; the tool rows it sits
// between stay ordinary rail entries.
const prose = container.querySelectorAll(".zen-fold-prose");
expect(prose).toHaveLength(1);
expect(prose[0].textContent).toContain("Trying the other config.");
expect(prose[0].textContent).not.toContain(
"The investigation is complete.",
);

// The answer renders the same markdown root, outside the demoted wrapper.
const answer = Array.from(
container.querySelectorAll(".agent-markdown"),
).find((el) => el.textContent?.includes("The investigation is complete."));
expect(answer).not.toBeUndefined();
expect(answer?.closest(".zen-fold-prose")).toBeNull();
});
});
30 changes: 29 additions & 1 deletion src/surfaces/AgentTranscript.interjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,17 @@ afterEach(() => {
vi.unstubAllGlobals();
});

// The labeled divider is the live rendering — the note lands while the turn
// is still going. A settled turn folds it into the work trail as one compact
// line instead, which the last test covers.
function render(text: string, severity: "blocker" | "concern" = "concern") {
const blocks: Block[] = [{
id: "advisor",
role: "system",
text,
interjection: { customType: "advisor", severity },
}];
act(() => root.render(createElement(AgentTranscript, { blocks, busy: false })));
act(() => root.render(createElement(AgentTranscript, { blocks, busy: true })));
}

const note = "Check the fallback.\n```ts\nconst result = read();\nif (!result) throw new Error('missing');\n```";
Expand Down Expand Up @@ -86,4 +89,29 @@ describe("AgentTranscript interjection preview", () => {
act(() => resize());
expect(container.querySelector("button")).toBeNull();
});

it("settles into the work trail as one compact line that opens to the full note", () => {
const blocks: Block[] = [{
id: "advisor",
role: "system",
text: note,
interjection: { customType: "advisor", severity: "concern" },
}];
act(() =>
root.render(createElement(AgentTranscript, { blocks, busy: false })),
);
// No divider, no clamped body: one line in the trail, named and
// severity-marked, that opens on a click.
expect(container.querySelector('[role="separator"]')).toBeNull();
const button = container.querySelector<HTMLButtonElement>(
'button[aria-expanded="false"]',
)!;
expect(button).not.toBeNull();
expect(button.textContent).toContain("Advisor");
expect(button.textContent).toContain("Concern");
expect(button.textContent).toContain("Check the fallback.");
act(() => button.click());
expect(button.getAttribute("aria-expanded")).toBe("true");
expect(container.querySelector("pre")?.textContent).toBe(note);
});
});
Loading
Loading