diff --git "a/piweb/hf-design.patch" "b/piweb/hf-design.patch" --- "a/piweb/hf-design.patch" +++ "b/piweb/hf-design.patch" @@ -3066,7 +3066,7 @@ index 0000000..8d003fe +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/globals.css b/app/globals.css -index d6b5535..96b3129 100644 +index d6b5535..401412e 100644 --- a/app/globals.css +++ b/app/globals.css @@ -24,46 +24,80 @@ @@ -3262,7 +3262,40 @@ index d6b5535..96b3129 100644 } /* WebKit reports dvh/innerHeight without the status-bar strip for installed -@@ -218,9 +288,9 @@ body { +@@ -135,28 +205,12 @@ html[data-theme="pine"], + } + } + +-.catppuccin-file-icon { ++/* File and folder icons in the tree and tabs (FileIcons.tsx): IBM Carbon icons ++ in the restrained text palette, not language colours. */ ++.file-type-icon { + display: block; + flex-shrink: 0; +- background: center / contain no-repeat var(--catppuccin-icon-light); +-} +-html.dark .catppuccin-file-icon { +- background-image: var(--catppuccin-icon-dark); +-} +- +-/* Use the SVG alpha channel as a mask so file icons follow the app's +- restrained text palette instead of Catppuccin's language-specific colors. */ +-@supports (mask-image: url("mask.svg")) or (-webkit-mask-image: url("mask.svg")) { +- .catppuccin-file-icon { +- background: var(--text-dim); +- mask: center / contain no-repeat var(--catppuccin-icon-light); +- -webkit-mask: center / contain no-repeat var(--catppuccin-icon-light); +- } +- html.dark .catppuccin-file-icon { +- background: var(--text-dim); +- mask-image: var(--catppuccin-icon-dark); +- -webkit-mask-image: var(--catppuccin-icon-dark); +- } ++ color: var(--text-dim); + } + + /* Theme toggle: circular wipe via View Transitions API. +@@ -218,9 +272,9 @@ body { margin-bottom: 22px; } @@ -3274,7 +3307,20 @@ index d6b5535..96b3129 100644 } .web-login-brand h1 { -@@ -826,8 +896,8 @@ button.extension-widget-trigger:focus-visible { +@@ -703,9 +757,9 @@ button.extension-widget-trigger:focus-visible { + + .extension-widget-placement-icon { + display: block; +- width: 8px; +- height: 6px; +- flex: 0 0 8px; ++ width: 10px; ++ height: 10px; ++ flex: 0 0 10px; + fill: currentColor; + } + +@@ -826,8 +880,8 @@ button.extension-widget-trigger:focus-visible { } :root { @@ -3285,7 +3331,7 @@ index d6b5535..96b3129 100644 --font-ui: var(--font-ui-default); --font-mono: var(--font-mono-default); --font-ui-weight: 400; -@@ -908,12 +978,11 @@ pre, code { +@@ -908,12 +962,11 @@ pre, code { } .markdown-body .markdown-inline-code { background: var(--bg-subtle); @@ -3299,7 +3345,7 @@ index d6b5535..96b3129 100644 } .markdown-body .contains-task-list { padding-left: 0; -@@ -967,8 +1036,8 @@ pre, code { +@@ -967,8 +1020,8 @@ pre, code { margin: 8px 0; overflow-x: auto; overflow-y: hidden; @@ -3310,7 +3356,7 @@ index d6b5535..96b3129 100644 } .markdown-body table { border-collapse: separate; -@@ -1108,16 +1177,13 @@ pre, code { +@@ -1108,16 +1161,13 @@ pre, code { min-width: 0; max-width: 100%; margin: 6px 0; @@ -3330,7 +3376,7 @@ index d6b5535..96b3129 100644 color: var(--text-dim); font-size: 11px; display: flex; -@@ -1503,12 +1569,11 @@ pre, code { +@@ -1503,12 +1553,11 @@ pre, code { height: 32px; padding: 0; box-sizing: border-box; @@ -3345,7 +3391,7 @@ index d6b5535..96b3129 100644 opacity: 0; visibility: hidden; pointer-events: none; -@@ -1584,22 +1649,21 @@ pre, code { +@@ -1584,22 +1633,21 @@ pre, code { .new-session-logo { flex: none; @@ -3374,7 +3420,7 @@ index d6b5535..96b3129 100644 line-height: 1.15; white-space: nowrap; } -@@ -1613,7 +1677,8 @@ pre, code { +@@ -1613,7 +1661,8 @@ pre, code { gap: 6px; min-width: 0; color: var(--text-dim); @@ -3384,7 +3430,7 @@ index d6b5535..96b3129 100644 line-height: 1.4; white-space: nowrap; } -@@ -1652,7 +1717,7 @@ pre, code { +@@ -1652,7 +1701,7 @@ pre, code { /* The ring inside the box, clear of the "→" and "·" beside it. */ .new-session-update:focus-visible { @@ -3393,7 +3439,7 @@ index d6b5535..96b3129 100644 outline-offset: -2px; } -@@ -1678,12 +1743,12 @@ pre, code { +@@ -1678,12 +1727,12 @@ pre, code { } .new-session-logo { @@ -3409,7 +3455,7 @@ index d6b5535..96b3129 100644 } } -@@ -1722,14 +1787,14 @@ pre, code { +@@ -1722,14 +1771,14 @@ pre, code { min-width: 0; height: 29px; padding: 0 10px; @@ -3426,7 +3472,7 @@ index d6b5535..96b3129 100644 } /* Above a fresh composer (inline): no box until hovered or open, 12px, the -@@ -1781,8 +1846,9 @@ pre, code { +@@ -1781,8 +1830,9 @@ pre, code { max-width: 28ch; overflow: hidden; color: var(--text-dim); @@ -3437,7 +3483,7 @@ index d6b5535..96b3129 100644 } .project-picker-parent { -@@ -1851,7 +1917,8 @@ pre, code { +@@ -1851,7 +1901,8 @@ pre, code { path's color and code type, cut at its end. */ .project-picker-label.is-alias { color: var(--text); @@ -3447,7 +3493,7 @@ index d6b5535..96b3129 100644 } /* A path or a branch, cut at its left (rtl), the text itself kept left to -@@ -1901,7 +1968,8 @@ pre, code { +@@ -1901,7 +1952,8 @@ pre, code { border-color: color-mix(in srgb, var(--accent) 40%, transparent); background: color-mix(in srgb, var(--accent) 6%, transparent); color: var(--text-dim); @@ -3457,7 +3503,7 @@ index d6b5535..96b3129 100644 } /* No worktree list here (the files tab): a box that says why, and does nothing. */ -@@ -2474,3 +2542,103 @@ span.token.table { +@@ -2474,3 +2526,103 @@ span.token.table { display: none !important; } } @@ -3628,10 +3674,10 @@ index 62c25e4..d333e66 100644 dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT, diff --git a/app/login/page.tsx b/app/login/page.tsx -index 80264fe..727ac43 100644 +index 80264fe..6506201 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx -@@ -1,6 +1,6 @@ +@@ -1,9 +1,11 @@ "use client"; -import Image from "next/image"; @@ -3639,7 +3685,12 @@ index 80264fe..727ac43 100644 import { useState, type FormEvent } from "react"; import { I18nProvider, useI18n } from "@/hooks/useI18n"; import { safeLoginDestination } from "@/lib/login-destination"; -@@ -49,7 +49,7 @@ function LoginForm() { ++import { CarbonIcon } from "@/components/CarbonIcon"; ++import * as Carbon from "@/components/carbon/icons"; + + function safeDestination(): string { + const destination = new URLSearchParams(window.location.search).get("next"); +@@ -49,7 +51,7 @@ function LoginForm() {
@@ -3648,6 +3699,18 @@ index 80264fe..727ac43 100644

Pi Web

{t("auth.prompt")}

+@@ -70,10 +72,7 @@ function LoginForm() { + disabled={busy} + /> + +
diff --git a/app/manifest.ts b/app/manifest.ts index 79c8282..97731f1 100644 --- a/app/manifest.ts @@ -4222,11 +4285,99 @@ index 86728a2..e5780e6 100644 color: var(--text); font: inherit; font-size: 12px; +diff --git a/components/AgentSessionPanel.tsx b/components/AgentSessionPanel.tsx +index a899c62..051b2df 100644 +--- a/components/AgentSessionPanel.tsx ++++ b/components/AgentSessionPanel.tsx +@@ -3,6 +3,8 @@ + import { useMemo, useState } from "react"; + import { useI18n } from "@/hooks/useI18n"; + import type { SessionInfo, SubagentSessionStatus } from "@/lib/types"; ++import { CarbonIcon } from "./CarbonIcon"; ++import * as Carbon from "./carbon/icons"; + + interface Props { + rootSession: SessionInfo; +@@ -48,22 +50,16 @@ export function StatusIcon({ status }: { status: SubagentSessionStatus }) { + } + if (status === "failed") { + return ( +- ++ + ); + } + if (status === "aborted" || status === "interrupted") { + return ( +- ++ + ); + } + return ( +- ++ + ); + } + +@@ -119,13 +115,9 @@ function AgentRow({ + > + + {main ? ( +- ++ + ) : ( +- ++ + )} + + +diff --git a/components/AgentsConfig.tsx b/components/AgentsConfig.tsx +index 861a1c6..e791a20 100644 +--- a/components/AgentsConfig.tsx ++++ b/components/AgentsConfig.tsx +@@ -38,6 +38,8 @@ import { + } from "./SettingsUi"; + import { ModelSelector } from "./ModelSelector"; + import { projectTrustReloadKey } from "./settings-ui-helpers"; ++import { CarbonIcon } from "./CarbonIcon"; ++import * as Carbon from "./carbon/icons"; + + const TOOL_OPTIONS = ["read", "bash", "edit", "write", "grep", "find", "ls"]; + const THINKING_OPTIONS = ["", "off", "minimal", "low", "medium", "high", "xhigh", "max"] as const; +@@ -727,9 +729,7 @@ export function AgentsConfig({ + className={savedOk ? "is-success" : undefined} + > + {savedOk && ( +- +- +- ++ + )} + {savedOk ? t("i18n.saved") : saving ? t("agents.saving") : t("agents.save")} + diff --git a/components/AppShell.tsx b/components/AppShell.tsx -index c8c4ba9..9f93c16 100644 +index c8c4ba9..ed04857 100644 --- a/components/AppShell.tsx +++ b/components/AppShell.tsx -@@ -1422,7 +1422,7 @@ export function AppShell() { +@@ -74,6 +74,8 @@ import type { FileViewerState } from "@/lib/file-viewer-state"; + import { PRESET_READ_ONLY, type ToolEntry } from "@/lib/tool-presets"; + import { getSessionFamily } from "@/lib/session-family"; + import { getLastSettingsSection, settingsSectionRequiresProject, type SettingsSection } from "@/lib/settings-navigation"; ++import { CarbonIcon } from "./CarbonIcon"; ++import * as Carbon from "./carbon/icons"; + + type SessionCopyField = "file" | "id" | "projectDir" | "gitBranch" | "gitWorktree"; + type AutoNameStatus = +@@ -1422,7 +1424,7 @@ export function AppShell() { onRunningSessionIdsChange={handleRunningSessionIdsChange} onSessionsChange={handleSessionsChange} /> @@ -4235,7 +4386,7 @@ index c8c4ba9..9f93c16 100644 {([ ["models", translate("common.models")], ["skills", translate("common.skills")], -@@ -1432,19 +1432,11 @@ export function AppShell() { +@@ -1432,19 +1434,11 @@ export function AppShell() { + ); +@@ -1542,17 +1518,16 @@ export function AppShell() { justifyContent: "center", gap: 6, width: mobile ? TOP_BAR_ICON_BUTTON_SIZE : undefined, @@ -4305,7 +4480,35 @@ index c8c4ba9..9f93c16 100644 whiteSpace: "nowrap", transition: "color 0.1s, background 0.1s, opacity 0.1s", }} -@@ -1625,14 +1613,13 @@ export function AppShell() { +@@ -1567,25 +1542,10 @@ export function AppShell() { + }} + data-mobile-toolbar-action={mobile ? "history" : undefined} + > +- ++ }} /> + {!mobile && {translate("history.label")}} + + {(() => { +@@ -1625,14 +1585,13 @@ export function AppShell() { style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 6, width: mobile ? TOP_BAR_ICON_BUTTON_SIZE : undefined, @@ -4323,7 +4526,25 @@ index c8c4ba9..9f93c16 100644 transition: "color 0.1s, background 0.1s, opacity 0.1s", }} onMouseEnter={(event) => { -@@ -1677,13 +1664,12 @@ export function AppShell() { +@@ -1652,15 +1611,9 @@ export function AppShell() { + + + ) : isSuccess ? ( +- ++ + ) : ( +- ++ + )} + {!mobile && {label}} + +@@ -1677,20 +1630,17 @@ export function AppShell() { position: "relative", display: "flex", alignItems: "center", justifyContent: "center", gap: 6, width: mobile ? TOP_BAR_ICON_BUTTON_SIZE : undefined, @@ -4340,7 +4561,15 @@ index c8c4ba9..9f93c16 100644 transition: "color 0.1s, background 0.1s", }} data-mobile-toolbar-action={mobile ? "agents" : undefined} -@@ -1714,11 +1700,10 @@ export function AppShell() { + > +- ++ + {!mobile && {translate("agentSwitcher.title")}} +
+@@ -1885,14 +1820,14 @@ export function AppShell() { gap: mobile ? 7 : 10, paddingLeft: mobile ? 6 : 12, paddingRight: mobile ? 6 : 12, @@ -4410,6 +4676,239 @@ index c8c4ba9..9f93c16 100644 whiteSpace: "nowrap", cursor: showChat ? "pointer" : "default", fontVariantNumeric: "tabular-nums", transition: "color 0.1s, background 0.1s", +@@ -1908,17 +1843,13 @@ export function AppShell() { + <> + {tokens && tokens.input > 0 && ( + +- ++ + {formatCompact(tokens.input)} + + )} + {tokens && tokens.output > 0 && ( + +- ++ + {formatCompact(tokens.output)} + + )} +@@ -1942,25 +1873,19 @@ export function AppShell() { + <> + {tokens && tokens.input > 0 && ( + +- ++ + {formatCompact(tokens.input)} + + )} + {tokens && tokens.output > 0 && ( + +- ++ + {formatCompact(tokens.output)} + + )} + {tokens && tokens.cacheRead > 0 && ( + +- ++ + {formatCompact(tokens.cacheRead)} + + )} +@@ -1971,9 +1896,7 @@ export function AppShell() { + )} + {desktopContextText && ( + +- ++ + {desktopContextText} + + )} +@@ -2011,9 +1934,7 @@ export function AppShell() { + onMouseEnter={(event) => { if (!covered) event.currentTarget.style.color = "var(--text)"; }} + onMouseLeave={(event) => { event.currentTarget.style.color = rightPanelOpen ? "var(--text)" : "var(--text-muted)"; }} + > +- ++ + + ); + }; +@@ -2178,13 +2099,9 @@ export function AppShell() { + onMouseLeave={(e) => { e.currentTarget.style.color = "var(--text-muted)"; }} + > + {sidebarOpen ? ( +- +- +- ++ + ) : ( +- +- +- ++ + )} + + {isMobile && ( +@@ -2221,13 +2138,9 @@ export function AppShell() { + }} + > + {mobileToolbarMoreOpen ? ( +- ++ + ) : ( +- ++ + )} + + )} +@@ -2443,14 +2356,9 @@ export function AppShell() { + }} + > + {copied ? ( +- ++ + ) : ( +- ++ + )} + + ); +@@ -2601,9 +2509,7 @@ export function AppShell() { + + ) : ( +
+- +- +- ++ +
+
{translate("workspace.getStarted")}
+
+@@ -2673,11 +2579,7 @@ export function AppShell() { + title={translate(rightPanelFullWidth ? "files.restorePanelWidth" : "files.expandPanel")} + aria-label={translate(rightPanelFullWidth ? "files.restorePanelWidth" : "files.expandPanel")} + > +- ++ + + +
+ +diff --git a/components/BranchNavigator.tsx b/components/BranchNavigator.tsx +index 0f8186b..e8bafeb 100644 +--- a/components/BranchNavigator.tsx ++++ b/components/BranchNavigator.tsx +@@ -3,6 +3,8 @@ + import { useState, useCallback, useMemo, useRef, useEffect } from "react"; + import type { BranchPreview, SessionEntry, SessionTreeNode } from "@/lib/types"; + import { useI18n } from "@/hooks/useI18n"; ++import { CarbonIcon } from "./CarbonIcon"; ++import * as Carbon from "./carbon/icons"; + + interface Props { + tree: SessionTreeNode[]; +@@ -304,18 +306,11 @@ export function BranchNavigator({ tree, activeLeafId, onLeafChange, inline, cont + const hasContent = !noBranchReason && topLevel.length > 0; + + const branchIcon = ( +- +- +- +- +- +- ++ + ); + + const chevron = ( +- +- +- ++ + ); + + +diff --git a/components/CarbonIcon.tsx b/components/CarbonIcon.tsx +new file mode 100644 +index 0000000..00d78bd +--- /dev/null ++++ b/components/CarbonIcon.tsx +@@ -0,0 +1,38 @@ ++import { createElement, type CSSProperties, type SVGProps } from "react"; ++import type { CarbonIconData } from "./carbon/icons"; ++ ++/** ++ * An IBM Carbon icon (components/carbon/icons.ts, generated by the design ++ * system's design/carbon-icons.mjs), filled with `color` or the current text ++ * colour. Decorative by default (`aria-hidden`); pass `label` when the icon is ++ * the only thing that says what it means. The pi mark is not an icon: it is ++ * PiLogo. ++ */ ++export interface CarbonIconProps extends Omit, "color" | "children"> { ++ icon: CarbonIconData; ++ /** Rendered width and height in px. */ ++ size?: number; ++ /** Fill; the current text colour when unset. */ ++ color?: string; ++ label?: string; ++ className?: string; ++ style?: CSSProperties; ++} ++ ++export function CarbonIcon({ icon, size = 16, color, label, className, style, ...rest }: CarbonIconProps) { ++ return ( ++ ++ {icon.shapes.map(([tag, attrs], index) => createElement(tag, { key: index, ...attrs }))} ++ ++ ); ++} diff --git a/components/ChatInput.test.mjs b/components/ChatInput.test.mjs index adfcc12..cb17981 100644 --- a/components/ChatInput.test.mjs @@ -4424,10 +4923,112 @@ index adfcc12..cb17981 100644 assert.doesNotMatch(html, /type="file"|Attach files|Change tool preset/); }); diff --git a/components/ChatInput.tsx b/components/ChatInput.tsx -index 9a0a1b7..b04d7c6 100644 +index 9a0a1b7..a7549d0 100644 --- a/components/ChatInput.tsx +++ b/components/ChatInput.tsx -@@ -2245,14 +2245,14 @@ export const ChatInput = memo(forwardRef(function ChatIn +@@ -35,6 +35,8 @@ import { useFontPreferences } from "@/hooks/useFontPreferences"; + import type { ToolPreset } from "@/lib/tool-presets"; + import { SelectorRow } from "./SelectorRow"; + import { ModelSelector, type ModelSelectorOption } from "./ModelSelector"; ++import { CarbonIcon } from "./CarbonIcon"; ++import * as Carbon from "./carbon/icons"; + + export { filterModelOptions } from "./ModelSelector"; + +@@ -572,22 +574,7 @@ function ModelNoticeBanner({ tone, title, body, onClose }: { tone: "error" | "wa + lineHeight: 1.45, + }} + > +- ++ +
+
{title}
+
{body}
+@@ -1795,10 +1782,7 @@ export const ChatInput = memo(forwardRef(function ChatIn + e.currentTarget.style.borderColor = "var(--border)"; + }} + > +- +- +- +- ++ + {t("chat.recall")} + + )} +@@ -1819,10 +1803,7 @@ export const ChatInput = memo(forwardRef(function ChatIn + borderRadius: 6, fontSize: 12, color: "rgba(180,130,0,0.9)", + display: "flex", alignItems: "center", gap: 6, + }}> +- +- +- +- ++ + {t("chat.retrying", { attempt: retryInfo.attempt, max: retryInfo.maxAttempts })}{retryInfo.errorMessage && — {retryInfo.errorMessage}} +
+ )} +@@ -1833,9 +1814,7 @@ export const ChatInput = memo(forwardRef(function ChatIn + borderRadius: 6, fontSize: 12, color: "rgba(5,150,105,0.95)", + display: "flex", alignItems: "center", gap: 6, + }}> +- +- +- ++ + {compactResultText} +
+ )} +@@ -1885,9 +1864,7 @@ export const ChatInput = memo(forwardRef(function ChatIn + cursor: "pointer", padding: 0, color: "var(--text-muted)", + }} + > +- +- +- ++ + +
+ ))} +@@ -1924,21 +1901,7 @@ export const ChatInput = memo(forwardRef(function ChatIn + color: "var(--text-dim)", + }} + > +- ++ + +
+ {inputHistory.map((item, index) => { +@@ -2245,14 +2208,14 @@ export const ChatInput = memo(forwardRef(function ChatIn flexDirection: compact ? "column" : "row", gap: 8, alignItems: compact ? "stretch" : "center", @@ -4450,7 +5051,7 @@ index 9a0a1b7..b04d7c6 100644 } as React.CSSProperties} >