Compare commits

..

4 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
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 81 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.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,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 by Preview beside Whole file
* 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
@@ -2060,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(
@@ -2559,8 +2562,11 @@ 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={() => {
// Changes and Whole file show an HTML page's source;
// only Preview runs it.
if (htmlShown) setMdPreview(false);
if (whole === w) return; 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.
@@ -2574,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"
}`} }`}
@@ -2582,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>
@@ -2993,7 +3019,8 @@ export function LaneChangesPanel({
{t("changes.annotate")} {t("changes.annotate")}
</button> </button>
)} )}
{whole && text && !dirty && (isMarkdown(openFile) || isHtml(openFile)) && ( {/* An HTML file's Preview sits beside Whole file instead. */}
{whole && text && !dirty && isMarkdown(openFile) && (
<button <button
type="button" type="button"
data-testid="lane-file-preview" data-testid="lane-file-preview"
@@ -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 Whole file", async () => {
vi.mocked(api.lanes.gitTree).mockResolvedValue({ vi.mocked(api.lanes.gitTree).mockResolvedValue({
available: true, available: true,
path: "", path: "",
@@ -564,12 +564,47 @@ 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");
expect(frame.getAttribute("srcdoc")).toContain("<p>deck</p>");
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(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"); 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");
await user.click(screen.getByTestId("lane-file-preview")); // Back to Changes: the diff again, not the page.
expect(await screen.findByTestId("lane-file-editor")).toHaveValue("<p>deck</p>"); await user.click(screen.getByRole("button", { name: "Changes", pressed: false }));
expect(screen.queryByTestId("html-frame")).toBeNull(); await screen.findByRole("button", { name: "Changes", pressed: true });
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 () => {
@@ -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.35 (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), and a third view
brings the page back — and a conflicted file opens in the beside them, **Changes | Whole file | Preview**, shows it as the page —
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.35",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "agenthub", "name": "agenthub",
"version": "2.8.33", "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.33", "version": "2.8.35",
"bin": { "bin": {
"agenthub": "bin/agenthub.js" "agenthub": "bin/agenthub.js"
}, },