Compare commits

..

2 Commits

Author SHA1 Message Date
nntrivi2001 4a82790734 chore: v2.8.35 — an HTML file's Preview sits beside Whole file 2026-10-29 10:47:39 +07:00
nntrivi2001 80000793f4 feat(files): an HTML file's Preview sits beside Whole file
The Changes panel's view toggle becomes Changes | Whole file | Preview for an
HTML file; the Preview/Raw button on the file's name row stays for markdown.
2026-10-29 10:47:39 +07:00
7 changed files with 46 additions and 27 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Claude Code Working Guide # Claude Code Working Guide
_Last synced: v2.8.34 (2026-10-29) — the Changes panel shows an HTML page only on Preview (also from Changes)._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session. _Last synced: v2.8.35 (2026-10-29) — an HTML file's Preview sits beside Whole file in the Changes panel._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session.
## Project mission ## Project mission
- Maintain a reliable local-first dashboard for Claude Code session monitoring. - Maintain a reliable local-first dashboard for Claude Code session monitoring.
@@ -14,7 +14,7 @@
* is unsaved): Changes folds the unchanged lines away, Whole file * is unsaved): Changes folds the unchanged lines away, Whole file
* shows them; any other file opens on its read-only diff; a * shows them; any other file opens on its read-only diff; a
* whole markdown file can also be read rendered (Preview), an HTML * whole markdown file can also be read rendered (Preview), an HTML
* file shown as the page on Preview, from Changes too * file shown as the page by Preview beside Whole file
* (`HtmlFrame`, its relative CSS, scripts and images read like * (`HtmlFrame`, its relative CSS, scripts and images read like
* downloads), and an * downloads), and an
* image file is shown as the picture (`GET …/git/raw`) * image file is shown as the picture (`GET …/git/raw`)
@@ -2061,6 +2061,8 @@ export function LaneChangesPanel({
const dirty = draft !== null && draft !== text?.lines.join("\n"); const dirty = draft !== null && draft !== text?.lines.join("\n");
// Rendered only where the toggle is offered: a whole markdown or HTML file. // Rendered only where the toggle is offered: a whole markdown or HTML file.
const mdShown = mdPreview && whole && (isMarkdown(openFile) || isHtml(openFile)); const mdShown = mdPreview && whole && (isMarkdown(openFile) || isHtml(openFile));
// The HTML page is showing (the Preview view of the Changes / Whole file / Preview group).
const htmlShown = mdShown && isHtml(openFile);
// An HTML page's relative CSS/scripts/images, read like a download — only // An HTML page's relative CSS/scripts/images, read like a download — only
// from the working tree: a commit's page would get today's files. // from the working tree: a commit's page would get today's files.
const htmlFetch = useMemo( const htmlFetch = useMemo(
@@ -2560,12 +2562,12 @@ export function LaneChangesPanel({
<button <button
key={String(w)} key={String(w)}
type="button" type="button"
aria-pressed={whole === w} aria-pressed={whole === w && !htmlShown}
onClick={() => { onClick={() => {
if (whole === w) return;
// Changes and Whole file show an HTML page's source; // Changes and Whole file show an HTML page's source;
// only Preview runs it. // only Preview runs it.
if (isHtml(openFile)) setMdPreview(false); if (htmlShown) setMdPreview(false);
if (whole === w) return;
// Both views of a working file are the same editor: the // Both views of a working file are the same editor: the
// draft goes along, unsaved or not. // draft goes along, unsaved or not.
if (editable && draft !== null) return setWhole(w); if (editable && draft !== null) return setWhole(w);
@@ -2578,7 +2580,7 @@ export function LaneChangesPanel({
}); });
}} }}
className={`rounded px-2 py-0.5 text-[11px] transition-colors ${ className={`rounded px-2 py-0.5 text-[11px] transition-colors ${
whole === w whole === w && !htmlShown
? "bg-surface-4 text-fg-primary" ? "bg-surface-4 text-fg-primary"
: "text-fg-secondary hover:text-fg-primary" : "text-fg-secondary hover:text-fg-primary"
}`} }`}
@@ -2586,6 +2588,26 @@ export function LaneChangesPanel({
{t(w ? "changes.wholeFile" : "changes.changesView")} {t(w ? "changes.wholeFile" : "changes.changesView")}
</button> </button>
))} ))}
{/* An HTML file's third view: the page itself. */}
{isHtml(openFile) && (
<button
type="button"
data-testid="lane-file-preview"
aria-pressed={htmlShown}
disabled={dirty}
onClick={() => {
setMdPreview(true);
setWhole(true);
}}
className={`rounded px-2 py-0.5 text-[11px] transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
htmlShown
? "bg-surface-4 text-fg-primary"
: "text-fg-secondary hover:text-fg-primary"
}`}
>
{t("changes.preview")}
</button>
)}
</div> </div>
)} )}
</div> </div>
@@ -2997,21 +3019,17 @@ export function LaneChangesPanel({
{t("changes.annotate")} {t("changes.annotate")}
</button> </button>
)} )}
{/* Offered on Changes too: from there it opens the rendered whole file. */} {/* An HTML file's Preview sits beside Whole file instead. */}
{(!whole || text) && !dirty && (isMarkdown(openFile) || isHtml(openFile)) && ( {whole && text && !dirty && isMarkdown(openFile) && (
<button <button
type="button" type="button"
data-testid="lane-file-preview" data-testid="lane-file-preview"
aria-pressed={whole && mdPreview} aria-pressed={mdPreview}
onClick={() => { onClick={() => setMdPreview((v) => !v)}
if (whole) return setMdPreview((v) => !v);
setMdPreview(true);
setWhole(true);
}}
className="flex shrink-0 items-center gap-1 self-center rounded border border-border-light px-1.5 py-0.5 text-[11px] text-fg-secondary hover:bg-surface-2 hover:text-fg-primary" className="flex shrink-0 items-center gap-1 self-center rounded border border-border-light px-1.5 py-0.5 text-[11px] text-fg-secondary hover:bg-surface-2 hover:text-fg-primary"
> >
{whole && mdPreview ? <Code2 className="h-3 w-3" /> : <Eye className="h-3 w-3" />} {mdPreview ? <Code2 className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
{whole && mdPreview ? t("changes.raw") : t("changes.preview")} {mdPreview ? t("changes.raw") : t("changes.preview")}
</button> </button>
)} )}
{!local && range !== "commit" && openFile && ( {!local && range !== "commit" && openFile && (
@@ -544,7 +544,7 @@ describe("LaneChangesPanel", () => {
expect(screen.queryByTestId("lane-file-editor")).toBeNull(); expect(screen.queryByTestId("lane-file-editor")).toBeNull();
}); });
it("opens a whole HTML file as its source, the page after Preview, the source after Raw", async () => { it("opens a whole HTML file as its source, the page after Preview, the source after Whole file", async () => {
vi.mocked(api.lanes.gitTree).mockResolvedValue({ vi.mocked(api.lanes.gitTree).mockResolvedValue({
available: true, available: true,
path: "", path: "",
@@ -569,7 +569,8 @@ describe("LaneChangesPanel", () => {
await user.click(screen.getByTestId("lane-file-preview")); await user.click(screen.getByTestId("lane-file-preview"));
const frame = await screen.findByTestId("html-frame"); const frame = await screen.findByTestId("html-frame");
expect(frame.getAttribute("srcdoc")).toContain("<p>deck</p>"); expect(frame.getAttribute("srcdoc")).toContain("<p>deck</p>");
await user.click(screen.getByTestId("lane-file-preview")); expect(screen.getByRole("button", { name: "Preview", pressed: true })).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Whole file", pressed: false }));
expect(await screen.findByTestId("lane-file-editor")).toHaveValue("<p>deck</p>"); expect(await screen.findByTestId("lane-file-editor")).toHaveValue("<p>deck</p>");
expect(screen.queryByTestId("html-frame")).toBeNull(); expect(screen.queryByTestId("html-frame")).toBeNull();
}); });
@@ -600,9 +601,9 @@ describe("LaneChangesPanel", () => {
const frame = await screen.findByTestId("html-frame"); const frame = await screen.findByTestId("html-frame");
expect(frame.getAttribute("srcdoc")).toContain("<p>deck</p>"); expect(frame.getAttribute("srcdoc")).toContain("<p>deck</p>");
expect(frame.getAttribute("sandbox")).not.toContain("allow-same-origin"); expect(frame.getAttribute("sandbox")).not.toContain("allow-same-origin");
// Back to Changes, then Whole file: the source again, not the page. // Back to Changes: the diff again, not the page.
await user.click(screen.getByRole("button", { name: "Changes", pressed: false })); await user.click(screen.getByRole("button", { name: "Changes", pressed: false }));
await user.click(screen.getByRole("button", { name: "Whole file", pressed: false })); await screen.findByRole("button", { name: "Changes", pressed: true });
await waitFor(() => expect(screen.queryByTestId("html-frame")).toBeNull()); await waitFor(() => expect(screen.queryByTestId("html-frame")).toBeNull());
}); });
@@ -7316,7 +7316,7 @@ exports[`screen snapshots > Settings — about 1`] = `
<span <span
class="text-status-warning/90" class="text-status-warning/90"
> >
UI build v2.8.34 (rebuild client to match) UI build v2.8.35 (rebuild client to match)
</span> </span>
</p> </p>
</div> </div>
+3 -3
View File
@@ -678,9 +678,9 @@ reads the workspace's git surface in five modes:
least one line tall (`MIN_ROW`), so an empty filler row keeps the two least one line tall (`MIN_ROW`), so an empty filler row keeps the two
columns aligned. An image file (png, jpg, gif, webp, avif, bmp, ico, svg) is columns aligned. An image file (png, jpg, gif, webp, avif, bmp, ico, svg) is
shown as the picture (`/git/raw`), an HTML file (`.html`/`.htm`) opens shown as the picture (`/git/raw`), an HTML file (`.html`/`.htm`) opens
like any file (Changes, or Whole file as source) with a **Preview** like any file (Changes, or Whole file as source), and a third view
button — on Changes too — that shows it as the page; **Raw** goes back to beside them, **Changes | Whole file | Preview**, shows it as the page —
its source — and a conflicted file opens in the and a conflicted file opens in the
conflict view. The page (here, in a Chat file preview, and on a conflict view. The page (here, in a Chat file preview, and on a
double-click in the File Explorer, whose right-click menu keeps double-click in the File Explorer, whose right-click menu keeps
**Download**) runs in the chat artifact's sandbox (`HtmlFrame.tsx`: **Download**) runs in the chat artifact's sandbox (`HtmlFrame.tsx`:
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "agenthub", "name": "agenthub",
"version": "2.8.34", "version": "2.8.35",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "agenthub", "name": "agenthub",
"version": "2.8.34", "version": "2.8.35",
"hasInstallScript": true, "hasInstallScript": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "agenthub", "name": "agenthub",
"version": "2.8.34", "version": "2.8.35",
"bin": { "bin": {
"agenthub": "bin/agenthub.js" "agenthub": "bin/agenthub.js"
}, },