mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
feat(runner): add chat history sidebar
This commit is contained in:
1 parent
a6b2ad79b7
commit
d81f36722e
5 files changed
+286
-58
No files matched your search
@@ -678,7 +678,7 @@ test.describe("Capability clean-room chat", () => {
|
||||
await open(page, "thread-baseline");
|
||||
const entry = page.getByTestId("surface-chat-link");
|
||||
await expect(entry).toBeVisible();
|
||||
await expect(entry).toHaveAttribute("href", "#/chat");
|
||||
await expect(entry).toHaveRole("button");
|
||||
await expect(entry).toContainText("New chat");
|
||||
});
|
||||
|
||||
@@ -821,9 +821,19 @@ test.describe("Capability clean-room chat", () => {
|
||||
await openCleanRoom(page, ["MCK-1000", "MCK-2000"]);
|
||||
await expect(page.locator(".pit-identifier")).toHaveText("MCK-1000");
|
||||
|
||||
await page.getByTestId("new-chat-button").click();
|
||||
await page.getByTestId("surface-chat-link").click();
|
||||
await expect(page.locator(".pit-identifier")).toHaveText("MCK-2000");
|
||||
await expect(page.getByTestId("clean-room-empty")).toBeVisible();
|
||||
const history = page.getByRole("region", { name: "Session history" });
|
||||
await expect(history.getByRole("button")).toHaveCount(2);
|
||||
|
||||
await history.getByRole("button", { name: /MCK-1000/ }).click();
|
||||
await expect(page.locator(".pit-identifier")).toHaveText("MCK-1000");
|
||||
await expect(page.getByTestId("composer-reason")).toHaveText("Historical session is read-only");
|
||||
|
||||
await history.getByRole("button", { name: /MCK-2000.*current/ }).click();
|
||||
await expect(page.locator(".pit-identifier")).toHaveText("MCK-2000");
|
||||
await expect(page.locator("#composer-input")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("a failed live start reports the failure instead of a fixture", async ({ page }) => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
type CapabilityCleanRoomIdentity,
|
||||
} from "./live-client";
|
||||
import { parseCapabilityRoute, capabilityRouteHref, type CapabilityRoute } from "./route";
|
||||
import { SurfaceNav } from "./SurfaceNav";
|
||||
import { SurfaceNav, type CapabilityChatHistoryItem } from "./SurfaceNav";
|
||||
import { TurnGroup } from "./ThreadItems";
|
||||
|
||||
const PANEL_OPEN_KEY = "paperclip-runner.capability.panel.open";
|
||||
@@ -48,6 +48,37 @@ const PENDING_ANNOUNCEMENT = "A request is waiting for your answer.";
|
||||
const ANSWERED_ANNOUNCEMENT = "Your answer was recorded.";
|
||||
const SETTLED_ANNOUNCEMENT = "The pending request is resolved.";
|
||||
const SCENARIOS = ["hb-baseline", "dp-documents", "ix-interactions", "ar-artifacts"];
|
||||
const CHAT_HISTORY_KEY = "paperclip-runner.capability.chat.history.v1";
|
||||
|
||||
interface StoredChatSession {
|
||||
sessionId: string;
|
||||
snapshot: CapabilityIssueThreadSnapshot;
|
||||
identity: CapabilityCleanRoomIdentity | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
function readChatHistory(): StoredChatSession[] {
|
||||
try {
|
||||
const parsed = JSON.parse(window.localStorage.getItem(CHAT_HISTORY_KEY) ?? "[]") as unknown;
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter((item): item is StoredChatSession =>
|
||||
typeof item === "object" && item !== null &&
|
||||
typeof (item as StoredChatSession).sessionId === "string" &&
|
||||
typeof (item as StoredChatSession).updatedAt === "string" &&
|
||||
typeof (item as StoredChatSession).snapshot === "object",
|
||||
).slice(0, 12);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function persistChatHistory(history: StoredChatSession[]): void {
|
||||
try {
|
||||
window.localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(history.slice(0, 12)));
|
||||
} catch {
|
||||
// History is a convenience; a blocked or full storage quota must not stop chat.
|
||||
}
|
||||
}
|
||||
|
||||
function readStoredNumber(key: string, fallback: number): number {
|
||||
try {
|
||||
@@ -156,6 +187,9 @@ export function App() {
|
||||
const [snapshot, setSnapshot] = useState<CapabilityIssueThreadSnapshot | null>(null);
|
||||
const [devtools, setDevtools] = useState<CapabilityDevtoolsSnapshot | null>(null);
|
||||
const [identity, setIdentity] = useState<CapabilityCleanRoomIdentity | null>(null);
|
||||
const [chatHistory, setChatHistory] = useState<StoredChatSession[]>(readChatHistory);
|
||||
const [historicSessionId, setHistoricSessionId] = useState<string | null>(null);
|
||||
const liveRoomRef = useRef<StoredChatSession | null>(null);
|
||||
/**
|
||||
* Which surface produced `snapshot`. A hash change commits the new route in
|
||||
* the same frame that still renders the old snapshot, so without this a
|
||||
@@ -219,6 +253,7 @@ export function App() {
|
||||
if (cancelled) return;
|
||||
rememberSession(response.sessionId, "cleanroom");
|
||||
setIdentity(response.identity ?? null);
|
||||
setHistoricSessionId(null);
|
||||
setSnapshot(response.view);
|
||||
setSnapshotSurface("chat");
|
||||
} catch (cause) {
|
||||
@@ -318,12 +353,29 @@ export function App() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!panelOpen || snapshot === null || route.mode !== "live") return;
|
||||
if (historicSessionId !== null) return;
|
||||
let cancelled = false;
|
||||
void capabilityLiveClient.devtools(snapshot.sessionId)
|
||||
.then((next) => { if (!cancelled) setDevtools(next); })
|
||||
.catch((cause) => { if (!cancelled) setActionError(describe(cause)); });
|
||||
return () => { cancelled = true; };
|
||||
}, [panelOpen, route.mode, snapshot?.renderedAt, snapshot?.sessionId]);
|
||||
}, [historicSessionId, panelOpen, route.mode, snapshot?.renderedAt, snapshot?.sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chat || snapshot === null || historicSessionId !== null) return;
|
||||
const record: StoredChatSession = {
|
||||
sessionId: snapshot.sessionId,
|
||||
snapshot,
|
||||
identity,
|
||||
updatedAt: snapshot.renderedAt,
|
||||
};
|
||||
liveRoomRef.current = record;
|
||||
setChatHistory((current) => {
|
||||
const next = [record, ...current.filter((item) => item.sessionId !== record.sessionId)].slice(0, 12);
|
||||
persistChatHistory(next);
|
||||
return next;
|
||||
});
|
||||
}, [chat, historicSessionId, identity, snapshot?.renderedAt, snapshot?.sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (snapshot === null) return;
|
||||
@@ -517,7 +569,9 @@ export function App() {
|
||||
const adoptCleanRoom = useCallback((next: Awaited<ReturnType<typeof capabilityLiveClient.newCleanRoom>>) => {
|
||||
rememberSession(next.sessionId, "cleanroom");
|
||||
setIdentity(next.identity ?? null);
|
||||
setHistoricSessionId(null);
|
||||
setSnapshot(next.view);
|
||||
setDevtools(null);
|
||||
setActionError(null);
|
||||
setAnnouncement(
|
||||
`New clean-room chat started on ${next.view.issue.identifier}. The previous session was closed.`,
|
||||
@@ -531,11 +585,39 @@ export function App() {
|
||||
abandonTurn();
|
||||
setAnnouncement("Starting a new clean-room chat…");
|
||||
void capabilityLiveClient
|
||||
.newCleanRoom(snapshot?.sessionId ?? null)
|
||||
.newCleanRoom(liveRoomRef.current?.sessionId ?? snapshot?.sessionId ?? null)
|
||||
.then(adoptCleanRoom)
|
||||
.catch((cause) => setActionError(describe(cause)));
|
||||
}, [abandonTurn, adoptCleanRoom, snapshot]);
|
||||
|
||||
const selectChatHistory = useCallback((sessionId: string) => {
|
||||
const live = liveRoomRef.current;
|
||||
if (live?.sessionId === sessionId) {
|
||||
setSnapshot(live.snapshot);
|
||||
setIdentity(live.identity);
|
||||
setDevtools(null);
|
||||
setHistoricSessionId(null);
|
||||
setActionError(null);
|
||||
return;
|
||||
}
|
||||
const archived = chatHistory.find((item) => item.sessionId === sessionId);
|
||||
if (archived === undefined) return;
|
||||
abandonTurn();
|
||||
setSnapshot({
|
||||
...archived.snapshot,
|
||||
composer: {
|
||||
state: "disabled",
|
||||
helper: null,
|
||||
reason: "Historical session is read-only",
|
||||
pendingInteractionId: null,
|
||||
},
|
||||
});
|
||||
setIdentity(archived.identity);
|
||||
setDevtools(null);
|
||||
setHistoricSessionId(sessionId);
|
||||
setActionError(null);
|
||||
}, [abandonTurn, chatHistory]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setConfirmReset(false);
|
||||
abandonTurn();
|
||||
@@ -633,11 +715,27 @@ export function App() {
|
||||
() => (snapshot === null ? 0 : capabilityDenialCount(snapshot.evidence, null)),
|
||||
[snapshot],
|
||||
);
|
||||
const historyItems: CapabilityChatHistoryItem[] = chatHistory.map((item) => ({
|
||||
sessionId: item.sessionId,
|
||||
identifier: item.snapshot.issue.identifier,
|
||||
title: item.snapshot.issue.title,
|
||||
updatedAt: item.updatedAt,
|
||||
current: liveRoomRef.current?.sessionId === item.sessionId,
|
||||
}));
|
||||
const surfaceNav = (
|
||||
<SurfaceNav
|
||||
surface={route.surface}
|
||||
history={historyItems}
|
||||
activeSessionId={snapshot?.sessionId ?? null}
|
||||
onNewChat={chat ? newChat : () => { window.location.hash = "#/chat"; }}
|
||||
onSelectHistory={selectChatHistory}
|
||||
/>
|
||||
);
|
||||
|
||||
if (error !== null) {
|
||||
return (
|
||||
<div className="pit-app" data-thread-state="failed" data-surface={route.surface}>
|
||||
<SurfaceNav surface={route.surface} />
|
||||
{surfaceNav}
|
||||
<main className="pit-app-error">
|
||||
<p className="pit-composer-reason" role="alert" data-testid="surface-error">
|
||||
{chat
|
||||
@@ -667,7 +765,7 @@ export function App() {
|
||||
if (snapshot === null) {
|
||||
return (
|
||||
<div className="pit-app" data-thread-state="loading" data-surface={route.surface}>
|
||||
<SurfaceNav surface={route.surface} />
|
||||
{surfaceNav}
|
||||
<main className="pit-app-error">
|
||||
<p className="pit-muted" role="status" data-testid="surface-loading">
|
||||
{chat
|
||||
@@ -696,14 +794,13 @@ export function App() {
|
||||
data-surface={route.surface}
|
||||
data-connection-state={snapshot.connection.state}
|
||||
>
|
||||
<SurfaceNav surface={route.surface} />
|
||||
{surfaceNav}
|
||||
|
||||
<IssueHeader
|
||||
snapshot={snapshot}
|
||||
scenarios={SCENARIOS}
|
||||
surface={route.surface}
|
||||
cleanRoomToken={identity?.token ?? null}
|
||||
onNewChat={newChat}
|
||||
evidenceOpen={panelOpen}
|
||||
denialCount={denialCount}
|
||||
segment={segment}
|
||||
@@ -946,7 +1043,7 @@ export function App() {
|
||||
{showPanel ? (
|
||||
<EvidencePanel
|
||||
snapshot={snapshot}
|
||||
{...(route.mode === "live" ? { devtools } : {})}
|
||||
{...(route.mode === "live" && historicSessionId === null ? { devtools } : {})}
|
||||
onForkRevision={(revision) => {
|
||||
abandonTurn();
|
||||
setActionError(null);
|
||||
@@ -974,7 +1071,7 @@ export function App() {
|
||||
}
|
||||
onClose={closeEvidence}
|
||||
onJumpToThread={jumpToThread}
|
||||
onInvokeTool={route.mode === "live" ? async (operationId, input) => {
|
||||
onInvokeTool={route.mode === "live" && historicSessionId === null ? async (operationId, input) => {
|
||||
setActionError(null);
|
||||
const next = await capabilityLiveClient.invokeTool(snapshot.sessionId, operationId, input);
|
||||
setSnapshot(next.view);
|
||||
|
||||
@@ -17,7 +17,6 @@ export interface IssueHeaderProps {
|
||||
surface?: "issue" | "chat";
|
||||
/** Clean-room tenant token, rendered so identity rotation is visible. */
|
||||
cleanRoomToken?: string | null;
|
||||
onNewChat?: () => void;
|
||||
evidenceOpen: boolean;
|
||||
denialCount: number;
|
||||
segment: "thread" | "evidence";
|
||||
@@ -35,7 +34,6 @@ export function IssueHeader(props: IssueHeaderProps) {
|
||||
scenarios,
|
||||
surface = "issue",
|
||||
cleanRoomToken = null,
|
||||
onNewChat,
|
||||
evidenceOpen,
|
||||
denialCount,
|
||||
segment,
|
||||
@@ -69,11 +67,7 @@ export function IssueHeader(props: IssueHeaderProps) {
|
||||
<StatusBadge status={snapshot.issue.status} />
|
||||
</div>
|
||||
<div className="pit-header-controls">
|
||||
{chat ? (
|
||||
<button type="button" className="pit-button" onClick={onNewChat} data-testid="new-chat-button">
|
||||
<Icon name="spark" /> New chat
|
||||
</button>
|
||||
) : (
|
||||
{!chat ? (
|
||||
<>
|
||||
<label className="pit-visually-hidden" htmlFor="scenario-picker">Scenario</label>
|
||||
<select className="pit-select pit-desktop-only" id="scenario-picker" value={snapshot.issue.fixtureProfile} onChange={(event) => onSelectScenario(event.target.value)} data-testid="scenario-picker">
|
||||
@@ -81,7 +75,7 @@ export function IssueHeader(props: IssueHeaderProps) {
|
||||
</select>
|
||||
<button type="button" className="pit-button pit-desktop-only" onClick={onReplay} data-testid="replay-button"><Icon name="play" /> Replay</button>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
<button type="button" className="pit-icon-button pit-desktop-only" data-variant="destructive" onClick={onReset} data-testid="reset-button" title={chat ? "Reset chat" : "Reset scenario"} aria-label={chat ? "Reset chat" : "Reset scenario"}><Icon name="reset" /></button>
|
||||
<button type="button" className="pit-icon-button" data-variant="destructive" onClick={onStop} disabled={!turnActive} data-testid="stop-button" title="Stop turn" aria-label="Stop turn"><Icon name="stop" /></button>
|
||||
<button type="button" className="pit-button pit-desktop-only" aria-expanded={evidenceOpen} onClick={onToggleEvidence} data-testid="evidence-toggle"><Icon name="evidence" /> DevTools{denialCount > 0 ? ` (${denialCount})` : ""}</button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CapabilitySurface } from "./route";
|
||||
import { Icon } from "./Icons";
|
||||
|
||||
/**
|
||||
* Top-level surface switch (Capability plan revision 5).
|
||||
@@ -11,26 +12,46 @@ import type { CapabilitySurface } from "./route";
|
||||
* These are real anchors on purpose. The entry must survive a hard refresh, be
|
||||
* copyable, and open in a new tab, which a button handler would not give.
|
||||
*/
|
||||
export function SurfaceNav({ surface }: { surface: CapabilitySurface }) {
|
||||
export interface CapabilityChatHistoryItem {
|
||||
sessionId: string;
|
||||
identifier: string;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
export function SurfaceNav({ surface, history = [], activeSessionId = null, onNewChat, onSelectHistory }: {
|
||||
surface: CapabilitySurface;
|
||||
history?: CapabilityChatHistoryItem[];
|
||||
activeSessionId?: string | null;
|
||||
onNewChat?: () => void;
|
||||
onSelectHistory?: (sessionId: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<nav className="pit-surface-nav" aria-label="Capability surfaces" data-testid="surface-nav">
|
||||
<a
|
||||
className="pit-surface-link"
|
||||
href="#/issue/hb-baseline"
|
||||
aria-current={surface === "issue" ? "page" : undefined}
|
||||
data-testid="surface-issue-link"
|
||||
>
|
||||
Scenario explorer
|
||||
</a>
|
||||
<a
|
||||
className="pit-surface-link"
|
||||
href="#/chat"
|
||||
aria-current={surface === "chat" ? "page" : undefined}
|
||||
data-testid="surface-chat-link"
|
||||
>
|
||||
New chat
|
||||
<span className="pit-surface-note">· clean room</span>
|
||||
</a>
|
||||
</nav>
|
||||
<aside className="pit-surface-nav" aria-label="Runner navigation" data-testid="surface-nav">
|
||||
<div className="pit-sidebar-brand"><Icon name="activity" /><span>Runner lab</span></div>
|
||||
<nav className="pit-sidebar-primary" aria-label="Capability surfaces">
|
||||
<a className="pit-surface-link" href="#/issue/hb-baseline" aria-current={surface === "issue" ? "page" : undefined} data-testid="surface-issue-link">
|
||||
<Icon name="timeline" /><span>Scenario explorer</span>
|
||||
</a>
|
||||
<button type="button" className="pit-surface-link" onClick={onNewChat} disabled={onNewChat === undefined} data-testid="surface-chat-link">
|
||||
<Icon name="spark" /><span>New chat</span>
|
||||
</button>
|
||||
</nav>
|
||||
{surface === "chat" ? (
|
||||
<section className="pit-session-history" aria-labelledby="session-history-title">
|
||||
<h2 id="session-history-title">Session history</h2>
|
||||
<div className="pit-session-history-list">
|
||||
{history.map((item) => (
|
||||
<button type="button" key={item.sessionId} className="pit-session-history-item" data-selected={activeSessionId === item.sessionId} onClick={() => onSelectHistory?.(item.sessionId)}>
|
||||
<span className="pit-session-history-title">{item.title}</span>
|
||||
<span>{item.identifier}{item.current ? " · current" : ""}</span>
|
||||
</button>
|
||||
))}
|
||||
{history.length === 0 ? <p className="pit-muted">No chats yet.</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -164,8 +164,11 @@ textarea {
|
||||
* composer stays pinned to the bottom of the thread column (§2.1).
|
||||
*/
|
||||
.pit-app {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
padding-left: 15rem;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
@@ -184,35 +187,59 @@ textarea {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------- surface nav */
|
||||
/* ------------------------------------------------------------- sidebar */
|
||||
|
||||
/*
|
||||
* Two primary paths, one rail. The links wrap rather than scroll horizontally,
|
||||
* because the narrow layout must never introduce a horizontal scrollbar (§10).
|
||||
*/
|
||||
.pit-surface-nav {
|
||||
flex: none;
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--pit-space-2);
|
||||
padding: var(--pit-space-2) var(--pit-space-4);
|
||||
flex-direction: column;
|
||||
width: 15rem;
|
||||
box-sizing: border-box;
|
||||
padding: var(--pit-space-3);
|
||||
background: var(--pit-surface-sunken);
|
||||
border-right: 1px solid var(--pit-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pit-sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--pit-space-2);
|
||||
min-height: var(--pit-touch-target);
|
||||
padding: 0 var(--pit-space-2);
|
||||
color: var(--pit-foreground);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pit-sidebar-brand .pit-icon {
|
||||
color: var(--pit-live);
|
||||
}
|
||||
|
||||
.pit-sidebar-primary {
|
||||
display: grid;
|
||||
gap: var(--pit-space-1);
|
||||
padding-bottom: var(--pit-space-4);
|
||||
border-bottom: 1px solid var(--pit-border);
|
||||
}
|
||||
|
||||
.pit-surface-link {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* The label and its qualifier are separate flex items, so the space between
|
||||
them has to be a gap: a leading space inside the qualifier collapses. */
|
||||
gap: var(--pit-space-1);
|
||||
gap: var(--pit-space-2);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: var(--pit-touch-target);
|
||||
padding: 0 var(--pit-space-3);
|
||||
border: 1px solid var(--pit-border);
|
||||
padding: 0 var(--pit-space-2);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--pit-radius-md);
|
||||
background: transparent;
|
||||
color: var(--pit-muted-foreground);
|
||||
font-size: var(--pit-text-xs);
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pit-surface-link:hover {
|
||||
@@ -226,12 +253,62 @@ textarea {
|
||||
color: var(--pit-foreground);
|
||||
}
|
||||
|
||||
.pit-surface-note {
|
||||
color: var(--pit-faint-foreground);
|
||||
.pit-session-history {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
padding-top: var(--pit-space-4);
|
||||
}
|
||||
|
||||
.pit-surface-link[aria-current="page"] .pit-surface-note {
|
||||
color: var(--pit-muted-foreground);
|
||||
.pit-session-history h2 {
|
||||
margin: 0 0 var(--pit-space-2);
|
||||
padding: 0 var(--pit-space-2);
|
||||
color: var(--pit-faint-foreground);
|
||||
font-size: var(--pit-text-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pit-session-history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--pit-space-1);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pit-session-history-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
width: 100%;
|
||||
padding: var(--pit-space-2);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--pit-radius-md);
|
||||
background: transparent;
|
||||
color: var(--pit-faint-foreground);
|
||||
font-size: var(--pit-text-xs);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pit-session-history-item:hover,
|
||||
.pit-session-history-item[data-selected="true"] {
|
||||
border-color: var(--pit-border);
|
||||
background: var(--pit-surface);
|
||||
}
|
||||
|
||||
.pit-session-history-item[data-selected="true"] {
|
||||
border-color: var(--pit-accent);
|
||||
}
|
||||
|
||||
.pit-session-history-title {
|
||||
overflow: hidden;
|
||||
color: var(--pit-foreground);
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* A load failure and the pre-session wait share the centred single-column. */
|
||||
@@ -1683,6 +1760,35 @@ textarea {
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.pit-app {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.pit-surface-nav {
|
||||
position: static;
|
||||
width: auto;
|
||||
flex: none;
|
||||
padding: var(--pit-space-1) var(--pit-space-2);
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--pit-border);
|
||||
}
|
||||
|
||||
.pit-sidebar-brand,
|
||||
.pit-session-history {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pit-sidebar-primary {
|
||||
display: flex;
|
||||
gap: var(--pit-space-1);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.pit-surface-link {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.pit-desktop-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user