Skip to content

Commit 032d2bd

Browse files
committed
fix(desktop): make markdown viewer context-aware
Signed-off-by: Clay Delk <clay.delk@gmail.com>
1 parent e5dc6dc commit 032d2bd

17 files changed

Lines changed: 471 additions & 102 deletions
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type * as React from "react";
2+
3+
import { MarkdownDocAuxiliaryPanel } from "@/features/channels/ui/MarkdownDocAuxiliaryPanel";
4+
import { RightAuxiliaryPane } from "@/features/channels/ui/RightAuxiliaryPane";
5+
import type { MarkdownDocTarget } from "@/shared/ui/markdown/markdownDocViewerContext";
6+
7+
type ChannelMarkdownDocPanelProps = {
8+
canResetWidth: boolean;
9+
doc: MarkdownDocTarget;
10+
onClose: () => void;
11+
onResetWidth: () => void;
12+
onResizeStart: (event: React.PointerEvent<HTMLButtonElement>) => void;
13+
stacked?: boolean;
14+
widthPx: number;
15+
};
16+
17+
/** Document pane shown beside, or stacked over, a still-mounted thread. */
18+
export function ChannelMarkdownDocPanel({
19+
canResetWidth,
20+
doc,
21+
onClose,
22+
onResetWidth,
23+
onResizeStart,
24+
stacked = false,
25+
widthPx,
26+
}: ChannelMarkdownDocPanelProps) {
27+
return (
28+
<RightAuxiliaryPane
29+
canResetWidth={canResetWidth}
30+
className={stacked ? "absolute inset-y-0 right-0 z-41" : undefined}
31+
onResetWidth={onResetWidth}
32+
onResizeStart={onResizeStart}
33+
testId={
34+
stacked ? "markdown-doc-stacked-panel" : "markdown-doc-third-panel"
35+
}
36+
widthPx={widthPx}
37+
>
38+
<MarkdownDocAuxiliaryPanel
39+
doc={doc}
40+
isSinglePanelView={false}
41+
onClose={onClose}
42+
useSplitAuxiliaryPane
43+
widthPx={widthPx}
44+
/>
45+
</RightAuxiliaryPane>
46+
);
47+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type * as React from "react";
2+
import { AnimatePresence } from "motion/react";
3+
4+
import { ChannelMarkdownDocPanel } from "@/features/channels/ui/ChannelMarkdownDocPanels";
5+
import type { MarkdownDocTarget } from "@/shared/ui/markdown/markdownDocViewerContext";
6+
7+
type Props = {
8+
canResetThreadPanelWidth: boolean;
9+
idleAuxiliarySurface: React.ReactNode;
10+
onCloseMarkdownDoc?: () => void;
11+
onResetThreadPanelWidth: () => void;
12+
onThreadPanelResizeStart: (
13+
event: React.PointerEvent<HTMLButtonElement>,
14+
) => void;
15+
openMarkdownDoc: MarkdownDocTarget | null;
16+
showIdleAuxiliaryOverThread: boolean;
17+
showMarkdownBesideThread: boolean;
18+
threadPanelWidthPx: number;
19+
threadSurface: { markExitComplete: () => void };
20+
useStackedMarkdownPanel: boolean;
21+
};
22+
23+
/** Presence boundaries for responsive Markdown panes around an open thread. */
24+
export function ChannelMarkdownDocSurfaces(props: Props) {
25+
const renderPanel = (stacked = false) =>
26+
props.openMarkdownDoc && props.onCloseMarkdownDoc ? (
27+
<ChannelMarkdownDocPanel
28+
canResetWidth={props.canResetThreadPanelWidth}
29+
doc={props.openMarkdownDoc}
30+
onClose={props.onCloseMarkdownDoc}
31+
onResetWidth={props.onResetThreadPanelWidth}
32+
onResizeStart={props.onThreadPanelResizeStart}
33+
stacked={stacked}
34+
widthPx={props.threadPanelWidthPx}
35+
/>
36+
) : null;
37+
return (
38+
<>
39+
<AnimatePresence>
40+
{props.showMarkdownBesideThread ? renderPanel() : null}
41+
</AnimatePresence>
42+
<AnimatePresence onExitComplete={props.threadSurface.markExitComplete}>
43+
{props.useStackedMarkdownPanel
44+
? renderPanel(true)
45+
: props.showIdleAuxiliaryOverThread
46+
? props.idleAuxiliarySurface
47+
: null}
48+
</AnimatePresence>
49+
</>
50+
);
51+
}

desktop/src/features/channels/ui/ChannelPane.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { AgentSessionThreadPanel } from "@/features/channels/ui/AgentSessionThre
3030
import { ChannelManagementAuxiliaryPanel } from "@/features/channels/ui/ChannelManagementAuxiliaryPanel";
3131
import { IdleAuxiliaryPanel } from "@/features/channels/ui/IdleAuxiliaryPanel";
3232
import { MarkdownDocAuxiliaryPanel } from "@/features/channels/ui/MarkdownDocAuxiliaryPanel";
33+
import { ChannelMarkdownDocSurfaces } from "@/features/channels/ui/ChannelMarkdownDocSurfaces";
3334
import { RightAuxiliaryPane } from "@/features/channels/ui/RightAuxiliaryPane";
3435
import { createChannelPaneAuxiliaryLayout } from "@/features/channels/ui/channelPaneAuxiliaryLayout";
3536
import {
@@ -43,6 +44,7 @@ import { getThreadPanelLayout } from "@/features/channels/lib/threadPanelLayout"
4344
import { useThreadViewMode } from "@/features/channels/lib/threadViewModePreference";
4445
import { useThreadViewModeSwitch } from "@/features/channels/ui/useThreadViewModeSwitch";
4546
import { useFocusDrawerPresence } from "@/features/channels/ui/useFocusDrawerPresence";
47+
import { AUXILIARY_PANEL_MIN_WIDTH_PX } from "@/shared/layout/AuxiliaryPanel";
4648
import { useChannelWorkingAgentPubkeys } from "@/features/agents/agentWorkingSignal";
4749
import { useCardMintJobs } from "@/features/agents/cardMintStore";
4850
import { BotActivityComposerAction } from "@/features/channels/ui/BotActivityBar";
@@ -85,6 +87,7 @@ export const ChannelPane = React.memo(function ChannelPane({
8587
onAutoSendComplete = null,
8688
botTypingEntries,
8789
channelManagementOpen = false,
90+
channelContentWidthPx,
8891
currentPubkey,
8992
editTarget = null,
9093
fetchOlder,
@@ -437,10 +440,15 @@ export const ChannelPane = React.memo(function ChannelPane({
437440
priorityIdleAuxiliary,
438441
replaceThreadWithIdleAuxiliary,
439442
showIdleAuxiliaryOverThread,
443+
showMarkdownBesideThread,
440444
useFocusIdleDrawer,
441445
useFocusThreadDrawer,
446+
useStackedMarkdownPanel,
442447
useSplitAuxiliaryPane,
443448
} = createChannelPaneAuxiliaryLayout({
449+
canFitThirdPanel:
450+
channelContentWidthPx >=
451+
threadPanelWidthPx * 2 + AUXILIARY_PANEL_MIN_WIDTH_PX,
444452
channelManagementOpen,
445453
hasAgentSession: Boolean(activeChannel && selectedAgent),
446454
hasIdleAuxiliaryPanel: Boolean(idleAuxiliaryPanel),
@@ -456,14 +464,16 @@ export const ChannelPane = React.memo(function ChannelPane({
456464
});
457465
const { channelIsCovered, markExitComplete } = useFocusDrawerPresence(
458466
useFocusThreadDrawer || useFocusIdleDrawer,
459-
priorityIdleAuxiliary
460-
? (onCloseIdleAuxiliaryPanel ?? onCloseThread)
461-
: useFocusThreadDrawer
462-
? onCloseThread
463-
: (onCloseIdleAuxiliaryPanel ?? onCloseThread),
467+
useStackedMarkdownPanel && onCloseMarkdownDoc
468+
? onCloseMarkdownDoc
469+
: priorityIdleAuxiliary
470+
? (onCloseIdleAuxiliaryPanel ?? onCloseThread)
471+
: useFocusThreadDrawer
472+
? onCloseThread
473+
: (onCloseIdleAuxiliaryPanel ?? onCloseThread),
464474
);
465475
const threadSurface = useThreadPanelSurface(
466-
showIdleAuxiliaryOverThread,
476+
showIdleAuxiliaryOverThread || useStackedMarkdownPanel,
467477
markExitComplete,
468478
);
469479
const { changeThreadViewMode, layoutScrollTargetId, resolveScrollTarget } =
@@ -989,9 +999,8 @@ export const ChannelPane = React.memo(function ChannelPane({
989999
idleAuxiliarySurface
9901000
)}
9911001
</AnimatePresence>
992-
<AnimatePresence onExitComplete={threadSurface.markExitComplete}>
993-
{showIdleAuxiliaryOverThread ? idleAuxiliarySurface : null}
994-
</AnimatePresence>
1002+
{/* biome-ignore format: line-count ratchet in this legacy component */}
1003+
<ChannelMarkdownDocSurfaces {...{ openMarkdownDoc, onCloseMarkdownDoc, threadSurface, canResetThreadPanelWidth, onResetThreadPanelWidth, onThreadPanelResizeStart, showMarkdownBesideThread, useStackedMarkdownPanel, showIdleAuxiliaryOverThread, idleAuxiliarySurface, threadPanelWidthPx }} />
9951004
</div>
9961005
);
9971006
});

desktop/src/features/channels/ui/ChannelPane.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type ChannelPaneProps = {
3636
onAutoSendComplete?: (() => void) | null;
3737
botTypingEntries: TypingIndicatorEntry[];
3838
channelManagementOpen?: boolean;
39+
/** Width of the channel content container, used for responsive pane topology. */
40+
channelContentWidthPx: number;
3941
currentPubkey?: string;
4042
editTarget?: MessageComposerEditTarget | null;
4143
fetchOlder?: () => Promise<void>;

desktop/src/features/channels/ui/ChannelScreen.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,7 @@ export function ChannelScreen({
833833
<GuardedChannelPane
834834
onOpenMarkdownDoc={handleOpenMarkdownDoc}
835835
activeChannel={activeChannel}
836+
channelContentWidthPx={channelContentWidthPx}
836837
activityAgents={channelAgentSessionAgents}
837838
agentPubkeys={agentPubkeys}
838839
agentPubkeysPending={agentPubkeysPending}

desktop/src/features/channels/ui/MarkdownDocAuxiliaryPanel.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { MarkdownDocTarget } from "@/shared/ui/markdown/markdownDocViewerCo
33

44
type MarkdownDocAuxiliaryPanelProps = {
55
doc: MarkdownDocTarget;
6+
/** Render chrome for the full focus drawer rather than a narrow pane. */
7+
isFocusDrawer?: boolean;
68
isSinglePanelView: boolean;
79
onClose: () => void;
810
useSplitAuxiliaryPane: boolean;
@@ -21,6 +23,7 @@ type MarkdownDocAuxiliaryPanelProps = {
2123
*/
2224
export function MarkdownDocAuxiliaryPanel({
2325
doc,
26+
isFocusDrawer = false,
2427
isSinglePanelView,
2528
onClose,
2629
useSplitAuxiliaryPane,
@@ -32,10 +35,13 @@ export function MarkdownDocAuxiliaryPanel({
3235
<MarkdownDocPanel
3336
key={doc.url}
3437
filename={doc.filename}
35-
isSinglePanelView={useSplitAuxiliaryPane ? false : isSinglePanelView}
36-
layout={useSplitAuxiliaryPane ? "split" : "standalone"}
38+
isFocusMode={isFocusDrawer}
39+
isSinglePanelView={
40+
isFocusDrawer || useSplitAuxiliaryPane ? false : isSinglePanelView
41+
}
42+
layout={useSplitAuxiliaryPane && !isFocusDrawer ? "split" : "standalone"}
3743
onClose={onClose}
38-
transparentChrome={useSplitAuxiliaryPane}
44+
transparentChrome={useSplitAuxiliaryPane && !isFocusDrawer}
3945
url={doc.url}
4046
widthPx={widthPx}
4147
/>

desktop/src/features/channels/ui/MarkdownDocPanel.tsx

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from "@/features/channels/ui/markdownDocFocus";
1515
import { useEscapeKey } from "@/shared/hooks/useEscapeKey";
1616
import { useIsThreadPanelOverlay } from "@/shared/hooks/use-mobile";
17+
import { cn } from "@/shared/lib/cn";
1718
import {
1819
AuxiliaryPanel,
1920
AuxiliaryPanelBody,
@@ -37,6 +38,8 @@ type MarkdownDocPanelProps = {
3738
url: string;
3839
/** Human-readable filename from the imeta `filename` field. */
3940
filename: string;
41+
/** Fill a parent focus drawer and center the document reading column. */
42+
isFocusMode?: boolean;
4043
isSinglePanelView?: boolean;
4144
layout?: "standalone" | "split";
4245
onClose: () => void;
@@ -68,6 +71,7 @@ function decodeErrorMessage(kind: "too-large" | "binary"): string {
6871
export function MarkdownDocPanel({
6972
url,
7073
filename,
74+
isFocusMode = false,
7175
isSinglePanelView = false,
7276
layout = "standalone",
7377
onClose,
@@ -125,7 +129,8 @@ export function MarkdownDocPanel({
125129

126130
return (
127131
<AuxiliaryPanel
128-
isSinglePanelView={isSinglePanelView}
132+
className={isFocusMode ? "w-full" : undefined}
133+
isSinglePanelView={isFocusMode || isSinglePanelView}
129134
layout={layout}
130135
onClose={onClose}
131136
testId="markdown-doc-panel"
@@ -155,13 +160,21 @@ export function MarkdownDocPanel({
155160
</AuxiliaryPanelHeader>
156161
}
157162
>
158-
<AuxiliaryPanelBody className="flex min-h-0 flex-col" panelPadding>
163+
<AuxiliaryPanelBody
164+
className="flex min-h-0 flex-col"
165+
panelPadding={!isFocusMode}
166+
>
159167
{/* The view picker gets its own pinned row below the title: sharing
160168
the title row squeezed the filename out, and the header chrome
161169
band overlays anything placed directly after it in the header
162170
slot — so the row lives inside the chrome-padded body instead. */}
163171
{decoded?.kind === "ok" ? (
164-
<div className="flex shrink-0 items-center px-4 pb-2">
172+
<div
173+
className={cn(
174+
"flex w-full shrink-0 items-center px-4 pb-2",
175+
isFocusMode && "mx-auto max-w-[55rem]",
176+
)}
177+
>
165178
<SegmentedControl
166179
legend="Document view"
167180
onValueChange={setView}
@@ -174,43 +187,45 @@ export function MarkdownDocPanel({
174187
</div>
175188
) : null}
176189
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-6">
177-
{docQuery.isPending ? (
178-
<div
179-
className="flex items-center justify-center py-12"
180-
data-testid="markdown-doc-loading"
181-
>
182-
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground/70" />
183-
</div>
184-
) : errorMessage !== null ? (
185-
<div className="flex flex-col items-center gap-3 py-12 text-center">
186-
<p className="text-sm text-muted-foreground">{errorMessage}</p>
187-
<Button onClick={handleDownload} size="sm" variant="secondary">
188-
<Download className="mr-1.5 h-4 w-4" />
189-
Download file
190-
</Button>
191-
</div>
192-
) : decoded?.kind === "ok" ? (
193-
view === "preview" ? (
194-
<Markdown
195-
blockCode
196-
className="pt-3 text-sm"
197-
content={decoded.text}
198-
hardLineBreaks={false}
199-
/>
200-
) : (
201-
<pre
202-
className="overflow-x-auto pt-3 text-xs leading-relaxed"
203-
data-testid="markdown-doc-code"
190+
<div className={cn("w-full", isFocusMode && "mx-auto max-w-[55rem]")}>
191+
{docQuery.isPending ? (
192+
<div
193+
className="flex items-center justify-center py-12"
194+
data-testid="markdown-doc-loading"
204195
>
205-
{/* Shiki's synchronous-tokenization guard caps highlighting at
206-
150 lines; longer documents render as plain text here. */}
207-
<SyntaxHighlightedCode
208-
code={decoded.text}
209-
language="markdown"
196+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground/70" />
197+
</div>
198+
) : errorMessage !== null ? (
199+
<div className="flex flex-col items-center gap-3 py-12 text-center">
200+
<p className="text-sm text-muted-foreground">{errorMessage}</p>
201+
<Button onClick={handleDownload} size="sm" variant="secondary">
202+
<Download className="mr-1.5 h-4 w-4" />
203+
Download file
204+
</Button>
205+
</div>
206+
) : decoded?.kind === "ok" ? (
207+
view === "preview" ? (
208+
<Markdown
209+
blockCode
210+
className="pt-3 text-sm"
211+
content={decoded.text}
212+
hardLineBreaks={false}
210213
/>
211-
</pre>
212-
)
213-
) : null}
214+
) : (
215+
<pre
216+
className="overflow-x-auto pt-3 text-xs leading-relaxed"
217+
data-testid="markdown-doc-code"
218+
>
219+
{/* Shiki's synchronous-tokenization guard caps highlighting at
220+
150 lines; longer documents render as plain text here. */}
221+
<SyntaxHighlightedCode
222+
code={decoded.text}
223+
language="markdown"
224+
/>
225+
</pre>
226+
)
227+
) : null}
228+
</div>
214229
</div>
215230
</AuxiliaryPanelBody>
216231
</AuxiliaryPanel>

0 commit comments

Comments
 (0)