Compare commits

...

2 Commits

Author SHA1 Message Date
nntrivi2001 39bea78a9c chore: v2.8.34 — HTML page on Preview in the Changes panel (also from Changes) 2026-10-29 10:38:15 +07:00
nntrivi2001 364fe4a286 fix(files): an HTML file in the Changes panel shows as the page only on Preview
Changes stays the default view and Whole file shows the source, as for any
file. A Preview button — offered on the Changes diff too, where it was
missing — opens the page; Raw, Changes or Whole file go back to the source.
2026-10-29 10:38:15 +07:00
7 changed files with 62 additions and 18 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Claude Code Working Guide # Claude Code Working Guide
_Last synced: v2.8.33 (2026-10-28) — an HTML file opens as the page (Files panel, Chat preview, File Explorer)._ 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.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.
## 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,8 +14,9 @@
* 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 opens as the page itself (`HtmlFrame`, its relative CSS, * file shown as the page on Preview, from Changes too
* scripts and images read like downloads), and an * (`HtmlFrame`, its relative CSS, scripts and images read like
* downloads), and an
* image file is shown as the picture (`GET …/git/raw`) * image file is shown as the picture (`GET …/git/raw`)
* history — recent commits; opening one shows that commit's diff * history — recent commits; opening one shows that commit's diff
* browse — quick-open by path, or the repo's tracked file tree (the * browse — quick-open by path, or the repo's tracked file tree (the
@@ -891,9 +892,9 @@ export function LaneChangesPanel({
const [whole, setWhole] = useState(false); const [whole, setWhole] = useState(false);
// A whole markdown or HTML file shown rendered instead of as source; per // A whole markdown or HTML file shown rendered instead of as source; per
// file, markdown opening as Settings → Appearance says // file, markdown opening as Settings → Appearance says
// (`lib/markdownView.ts`), an HTML page always as the page. // (`lib/markdownView.ts`), an HTML page only once Preview is clicked.
const [mdPreview, setMdPreview] = useState(readMarkdownPreview); const [mdPreview, setMdPreview] = useState(readMarkdownPreview);
useEffect(() => setMdPreview(isHtml(openFile) || readMarkdownPreview()), [openFile]); useEffect(() => setMdPreview(!isHtml(openFile) && readMarkdownPreview()), [openFile]);
const [data, setData] = useState<LaneChanges | null>(null); const [data, setData] = useState<LaneChanges | null>(null);
// A folder that is not a repo (an empty new workspace, a parent of clones) // A folder that is not a repo (an empty new workspace, a parent of clones)
// has no changes, review or history — only its files, which Files reads // has no changes, review or history — only its files, which Files reads
@@ -2562,6 +2563,9 @@ export function LaneChangesPanel({
aria-pressed={whole === w} aria-pressed={whole === w}
onClick={() => { onClick={() => {
if (whole === w) return; if (whole === w) return;
// Changes and Whole file show an HTML page's source;
// only Preview runs it.
if (isHtml(openFile)) setMdPreview(false);
// 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);
@@ -2993,16 +2997,21 @@ export function LaneChangesPanel({
{t("changes.annotate")} {t("changes.annotate")}
</button> </button>
)} )}
{whole && text && !dirty && (isMarkdown(openFile) || isHtml(openFile)) && ( {/* Offered on Changes too: from there it opens the rendered whole file. */}
{(!whole || text) && !dirty && (isMarkdown(openFile) || isHtml(openFile)) && (
<button <button
type="button" type="button"
data-testid="lane-file-preview" data-testid="lane-file-preview"
aria-pressed={mdPreview} aria-pressed={whole && mdPreview}
onClick={() => setMdPreview((v) => !v)} onClick={() => {
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"
> >
{mdPreview ? <Code2 className="h-3 w-3" /> : <Eye className="h-3 w-3" />} {whole && mdPreview ? <Code2 className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
{mdPreview ? t("changes.raw") : t("changes.preview")} {whole && 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 the page, its source after Raw", async () => { it("opens a whole HTML file as its source, the page after Preview, the source after Raw", async () => {
vi.mocked(api.lanes.gitTree).mockResolvedValue({ vi.mocked(api.lanes.gitTree).mockResolvedValue({
available: true, available: true,
path: "", path: "",
@@ -564,14 +564,48 @@ describe("LaneChangesPanel", () => {
render(<LaneChangesPanel laneId={7} full />); render(<LaneChangesPanel laneId={7} full />);
const sidebar = await screen.findByTestId("lane-changes-sidebar"); const sidebar = await screen.findByTestId("lane-changes-sidebar");
await user.click(await within(sidebar).findByText("deck.html")); await user.click(await within(sidebar).findByText("deck.html"));
expect(await screen.findByTestId("lane-file-editor")).toHaveValue("<p>deck</p>");
expect(screen.queryByTestId("html-frame")).toBeNull();
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>");
expect(frame.getAttribute("sandbox")).not.toContain("allow-same-origin");
await user.click(screen.getByTestId("lane-file-preview")); await user.click(screen.getByTestId("lane-file-preview"));
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();
}); });
it("opens a changed HTML file on its Changes, the page only after Preview", async () => {
vi.mocked(api.lanes.changes).mockResolvedValue(
changes({
files: [
{ path: "deck.html", status: "??", untracked: true, added: 1, deleted: 0, binary: false },
],
})
);
vi.mocked(api.lanes.gitFile).mockResolvedValue({
available: true,
path: "deck.html",
rev: "work",
binary: false,
truncated: false,
lines: ["<p>deck</p>"],
sha: "c".repeat(40),
});
const user = userEvent.setup();
render(<LaneChangesPanel laneId={7} />);
await user.click(await screen.findByTitle("deck.html"));
await screen.findByRole("button", { name: "Changes", pressed: true });
expect(screen.queryByTestId("html-frame")).toBeNull();
await user.click(await screen.findByTestId("lane-file-preview"));
const frame = await screen.findByTestId("html-frame");
expect(frame.getAttribute("srcdoc")).toContain("<p>deck</p>");
expect(frame.getAttribute("sandbox")).not.toContain("allow-same-origin");
// Back to Changes, then Whole file: the source again, not the page.
await user.click(screen.getByRole("button", { name: "Changes", pressed: false }));
await user.click(screen.getByRole("button", { name: "Whole file", pressed: false }));
await waitFor(() => expect(screen.queryByTestId("html-frame")).toBeNull());
});
it("shows an image file as the picture, with no edit control", async () => { it("shows an image file as the picture, with no edit control", async () => {
vi.mocked(api.lanes.gitTree).mockResolvedValue({ vi.mocked(api.lanes.gitTree).mockResolvedValue({
available: true, available: true,
@@ -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.33 (rebuild client to match) UI build v2.8.34 (rebuild client to match)
</span> </span>
</p> </p>
</div> </div>
+3 -2
View File
@@ -678,8 +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
as the page itself — **Raw** shows its source in the editor, **Preview** like any file (Changes, or Whole file as source) with a **Preview**
brings the page back — and a conflicted file opens in the button — on Changes too — that shows it as the page; **Raw** goes back to
its source — 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.33", "version": "2.8.34",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "agenthub", "name": "agenthub",
"version": "2.8.33", "version": "2.8.34",
"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.33", "version": "2.8.34",
"bin": { "bin": {
"agenthub": "bin/agenthub.js" "agenthub": "bin/agenthub.js"
}, },