Skip to content

Commit 0434778

Browse files
committed
Add fullscreen review display mode
1 parent 80fb448 commit 0434778

3 files changed

Lines changed: 218 additions & 4 deletions

File tree

src/ui/review-payload.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface PayloadRendererOptions {
1111
hostContext?: HostContext;
1212
errorMessage?: string | null;
1313
visibleFileCount?: number;
14+
presentation?: "inline" | "fullscreen";
1415
}
1516

1617
interface MountedPayload {

src/ui/workspace-app.css

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,49 @@ body {
230230
gap: 3px;
231231
}
232232

233+
.review-header-actions,
234+
.review-fullscreen-actions,
235+
.review-fullscreen-title {
236+
display: flex;
237+
align-items: center;
238+
}
239+
240+
.review-header-actions,
241+
.review-fullscreen-actions {
242+
justify-content: flex-end;
243+
gap: 12px;
244+
}
245+
246+
.review-button {
247+
min-height: 34px;
248+
padding: 0 13px;
249+
border: 1px solid color-mix(in srgb, var(--color-border-primary, #3a3a40) 92%, transparent);
250+
border-radius: 9px;
251+
background: color-mix(in srgb, var(--color-background-tertiary, #333338) 88%, transparent);
252+
color: var(--color-text-primary, #f5f5f6);
253+
cursor: pointer;
254+
font: inherit;
255+
font-size: var(--font-text-sm-size, 13px);
256+
font-weight: 500;
257+
}
258+
259+
.review-button:hover:not(:disabled) {
260+
background: color-mix(in srgb, var(--color-background-tertiary, #414148) 96%, transparent);
261+
}
262+
263+
.review-button:disabled {
264+
cursor: wait;
265+
opacity: 0.7;
266+
}
267+
268+
.review-mode-error {
269+
padding: 8px 14px;
270+
border-top: 1px solid color-mix(in srgb, var(--color-border-danger, #7f3030) 56%, transparent);
271+
background: color-mix(in srgb, var(--color-background-danger, #421e1e) 24%, transparent);
272+
color: var(--color-danger-text, #ee7676);
273+
font-size: var(--font-text-sm-size, 12px);
274+
}
275+
233276
.review-summary {
234277
display: grid;
235278
border-top: 1px solid var(--color-border-primary, #3a3a40);
@@ -279,6 +322,48 @@ body {
279322
flex: none;
280323
}
281324

325+
.review-fullscreen-shell {
326+
height: 100vh;
327+
height: 100dvh;
328+
}
329+
330+
.review-fullscreen {
331+
display: grid;
332+
grid-template-rows: auto auto minmax(0, 1fr);
333+
width: 100%;
334+
height: 100%;
335+
overflow: hidden;
336+
background: var(--color-background-primary, #18191d);
337+
color: var(--color-text-primary, #f5f5f6);
338+
}
339+
340+
.review-fullscreen-header {
341+
display: flex;
342+
align-items: center;
343+
justify-content: space-between;
344+
gap: 20px;
345+
min-height: 68px;
346+
padding: 12px 18px;
347+
border-bottom: 1px solid var(--color-border-primary, #3a3a40);
348+
background: color-mix(in srgb, var(--color-background-secondary, #28282d) 94%, transparent);
349+
}
350+
351+
.review-fullscreen-title {
352+
min-width: 0;
353+
gap: 12px;
354+
}
355+
356+
.review-fullscreen-body {
357+
min-width: 0;
358+
min-height: 0;
359+
overflow: hidden;
360+
}
361+
362+
.review-fullscreen .review-diff {
363+
height: 100%;
364+
max-height: none;
365+
}
366+
282367
.review-diff {
283368
display: grid;
284369
gap: 8px;

src/ui/workspace-app.tsx

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ interface MountedPayload {
3939
hostContext?: HostContext;
4040
errorMessage?: string | null;
4141
visibleFileCount?: number;
42+
presentation?: "inline" | "fullscreen";
4243
}): void;
4344
unmount(): void;
4445
}
@@ -50,6 +51,8 @@ let hostContext: HostContext | undefined;
5051
let card: ToolResultCard | null = null;
5152
let expanded = false;
5253
let reviewFilesExpanded = false;
54+
let reviewDisplayModePending = false;
55+
let reviewDisplayModeError: string | null = null;
5356
let errorMessage: string | null = null;
5457
let currentPayload: MountedPayload | null = null;
5558
let currentPayloadContainer: HTMLElement | null = null;
@@ -84,6 +87,8 @@ async function boot(): Promise<void> {
8487
card = null;
8588
expanded = false;
8689
reviewFilesExpanded = false;
90+
reviewDisplayModePending = false;
91+
reviewDisplayModeError = null;
8792
errorMessage = "No result card is available for this tool result.";
8893
render();
8994
return;
@@ -92,16 +97,27 @@ async function boot(): Promise<void> {
9297
card = { ...structured, tool };
9398
expanded = false;
9499
reviewFilesExpanded = false;
100+
reviewDisplayModePending = false;
101+
reviewDisplayModeError = null;
95102
errorMessage = null;
96103
render();
97104
};
98105

99106
app.onhostcontextchanged = (ctx) => {
107+
const previousDisplayMode = hostContext?.displayMode;
100108
hostContext = {
101109
...hostContext,
102110
...ctx,
103111
};
104112
applyHostContext();
113+
if (
114+
previousDisplayMode !== hostContext.displayMode &&
115+
card &&
116+
isReviewTool(card.tool)
117+
) {
118+
render();
119+
return;
120+
}
105121
renderPayloadIfNeeded();
106122
};
107123

@@ -265,12 +281,23 @@ async function renderPayloadIfNeeded(): Promise<void> {
265281
}
266282

267283
if (isReviewTool(card.tool) || isPatchTool(card.tool)) {
268-
const visibleFileCount = isReviewTool(card.tool) && !reviewFilesExpanded
284+
const presentation = isReviewTool(card.tool) && hostContext?.displayMode === "fullscreen"
285+
? "fullscreen"
286+
: "inline";
287+
const visibleFileCount = isReviewTool(card.tool) &&
288+
presentation === "inline" &&
289+
!reviewFilesExpanded
269290
? Math.max(3, (card.files ?? []).slice(0, 3).length)
270291
: undefined;
271292

272293
if (currentPayload) {
273-
currentPayload.update({ card, hostContext, errorMessage, visibleFileCount });
294+
currentPayload.update({
295+
card,
296+
hostContext,
297+
errorMessage,
298+
visibleFileCount,
299+
presentation,
300+
});
274301
return;
275302
}
276303

@@ -284,6 +311,7 @@ async function renderPayloadIfNeeded(): Promise<void> {
284311
hostContext,
285312
errorMessage,
286313
visibleFileCount,
314+
presentation,
287315
});
288316
return;
289317
}
@@ -389,6 +417,11 @@ function renderSummaryBadge(card: ToolResultCard): HTMLElement {
389417
function renderReviewCard(card: ToolResultCard, display: ToolDisplay): void {
390418
unmountPayload();
391419

420+
if (hostContext?.displayMode === "fullscreen") {
421+
renderFullscreenReview(card, display);
422+
return;
423+
}
424+
392425
const files = card.files ?? [];
393426
const hiddenCount = Math.max(0, files.length - 3);
394427
const main = element("main", { className: "shell" });
@@ -402,7 +435,24 @@ function renderReviewCard(card: ToolResultCard, display: ToolDisplay): void {
402435
element("span", { className: "tool-title", text: display.title }),
403436
element("span", { className: "tool-label", text: display.label, title: display.label }),
404437
);
405-
header.append(icon, titleGroup, renderSummaryBadge(card));
438+
439+
const headerActions = element("div", { className: "review-header-actions" });
440+
headerActions.append(renderSummaryBadge(card));
441+
if (files.length > 0 && canRequestDisplayMode("fullscreen")) {
442+
const reviewButton = element("button", {
443+
className: "review-button",
444+
type: "button",
445+
text: reviewDisplayModePending ? "Opening…" : "Review",
446+
disabled: reviewDisplayModePending,
447+
});
448+
reviewButton.setAttribute("aria-busy", String(reviewDisplayModePending));
449+
reviewButton.addEventListener("click", () => {
450+
void requestReviewDisplayMode("fullscreen");
451+
});
452+
headerActions.append(reviewButton);
453+
}
454+
455+
header.append(icon, titleGroup, headerActions);
406456

407457
const body = element("div", { className: "review-summary" });
408458
currentPayloadContainer = body;
@@ -430,7 +480,14 @@ function renderReviewCard(card: ToolResultCard, display: ToolDisplay): void {
430480
actions.append(toggleFiles);
431481
}
432482

433-
section.append(header, body);
483+
section.append(header);
484+
if (reviewDisplayModeError) {
485+
section.append(element("div", {
486+
className: "review-mode-error",
487+
text: reviewDisplayModeError,
488+
}));
489+
}
490+
section.append(body);
434491
if (actions.childElementCount > 0) {
435492
section.append(actions);
436493
}
@@ -440,6 +497,77 @@ function renderReviewCard(card: ToolResultCard, display: ToolDisplay): void {
440497
renderPayloadIfNeeded();
441498
}
442499

500+
function renderFullscreenReview(card: ToolResultCard, display: ToolDisplay): void {
501+
const main = element("main", { className: "shell review-fullscreen-shell" });
502+
const section = element("section", { className: "review-fullscreen" });
503+
const header = element("header", { className: "review-fullscreen-header" });
504+
const titleGroup = element("div", { className: "review-fullscreen-title" });
505+
const icon = element("span", { className: "tool-icon", ariaHidden: "true" });
506+
icon.innerHTML = display.icon;
507+
508+
const heading = element("div", { className: "review-title-group" });
509+
heading.append(
510+
element("span", { className: "tool-title", text: "Review changes" }),
511+
element("span", { className: "tool-label", text: display.label, title: display.label }),
512+
);
513+
titleGroup.append(icon, heading);
514+
515+
const actions = element("div", { className: "review-fullscreen-actions" });
516+
const closeButton = element("button", {
517+
className: "review-button",
518+
type: "button",
519+
text: reviewDisplayModePending ? "Closing…" : "Close review",
520+
disabled: reviewDisplayModePending,
521+
});
522+
closeButton.setAttribute("aria-busy", String(reviewDisplayModePending));
523+
closeButton.addEventListener("click", () => {
524+
void requestReviewDisplayMode("inline");
525+
});
526+
actions.append(renderSummaryBadge(card), closeButton);
527+
header.append(titleGroup, actions);
528+
529+
const body = element("div", { className: "review-fullscreen-body" });
530+
currentPayloadContainer = body;
531+
section.append(header);
532+
if (reviewDisplayModeError) {
533+
section.append(element("div", {
534+
className: "review-mode-error",
535+
text: reviewDisplayModeError,
536+
}));
537+
}
538+
section.append(body);
539+
main.append(section);
540+
appRoot.replaceChildren(main);
541+
renderPayloadIfNeeded();
542+
}
543+
544+
function canRequestDisplayMode(mode: "inline" | "fullscreen"): boolean {
545+
return Boolean(hostContext?.availableDisplayModes?.includes(mode));
546+
}
547+
548+
async function requestReviewDisplayMode(mode: "inline" | "fullscreen"): Promise<void> {
549+
if (!app || reviewDisplayModePending) return;
550+
551+
reviewDisplayModePending = true;
552+
reviewDisplayModeError = null;
553+
render();
554+
555+
try {
556+
const result = await app.requestDisplayMode({ mode });
557+
hostContext = {
558+
...hostContext,
559+
displayMode: result.mode,
560+
};
561+
} catch (requestError) {
562+
reviewDisplayModeError = requestError instanceof Error
563+
? requestError.message
564+
: "Unable to change the review display mode.";
565+
} finally {
566+
reviewDisplayModePending = false;
567+
render();
568+
}
569+
}
570+
443571
function renderChevron(isExpanded: boolean, visible: boolean): HTMLElement {
444572
const chevron = element("span", {
445573
className: visible ? `chevron ${isExpanded ? "expanded" : ""}` : "chevron",

0 commit comments

Comments
 (0)