diff --git "a/piweb/hf-design.patch" "b/piweb/hf-design.patch" --- "a/piweb/hf-design.patch" +++ "b/piweb/hf-design.patch" @@ -1994,7 +1994,7 @@ index 0000000..36662a0 +} diff --git a/app/focus.test.mjs b/app/focus.test.mjs new file mode 100644 -index 0000000..80ad787 +index 0000000..01b1f80 --- /dev/null +++ b/app/focus.test.mjs @@ -0,0 +1,135 @@ @@ -2103,8 +2103,8 @@ index 0000000..80ad787 + // A 7px status dot fills the 16px slot; a provider's models and "+ Model" start under its name. + assert.match(settings, /\.config-sidebar-item > \.config-status-dot \{ margin-inline: 4\.5px; \}/); + assert.match(settings, /\.models-sidebar-indented-item \{[^}]*padding-left: 32px;/); -+ assert.match(ui, //); -+ assert.doesNotMatch(models, /Carbon\.Chip\} size=\{11\}/, "a models.json provider's chip is a 16px mark like a managed provider's logo"); ++ assert.match(ui, //); ++ assert.doesNotMatch(models, /Lucide\.Cpu\} size=\{11\}/, "a models.json provider's chip is a 16px mark like a managed provider's logo"); +}); + +test("section labels are quiet and in sentence case, never small bold capitals", () => { @@ -4460,7 +4460,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 49e70be..f1e77b4 100644 +index 49e70be..7dcadeb 100644 --- a/app/globals.css +++ b/app/globals.css @@ -24,46 +24,109 @@ @@ -4849,7 +4849,7 @@ index 49e70be..f1e77b4 100644 } -.catppuccin-file-icon { -+/* File and folder icons in the tree and tabs (FileIcons.tsx): IBM Carbon icons ++/* File and folder icons in the tree and tabs (FileIcons.tsx): Lucide icons + in the restrained text palette, not language colours. */ +.file-type-icon { display: block; @@ -7142,7 +7142,7 @@ index 0000000..099fc49 + } +}); diff --git a/app/login/page.tsx b/app/login/page.tsx -index 80264fe..6506201 100644 +index 80264fe..a5afca5 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,9 +1,11 @@ @@ -7153,8 +7153,8 @@ index 80264fe..6506201 100644 import { useState, type FormEvent } from "react"; import { I18nProvider, useI18n } from "@/hooks/useI18n"; import { safeLoginDestination } from "@/lib/login-destination"; -+import { CarbonIcon } from "@/components/CarbonIcon"; -+import * as Carbon from "@/components/carbon/icons"; ++import { LucideIcon } from "@/components/LucideIcon"; ++import * as Lucide from "@/components/lucide/icons"; function safeDestination(): string { const destination = new URLSearchParams(window.location.search).get("next"); @@ -7175,7 +7175,7 @@ index 80264fe..6506201 100644 - - - -+ ++ {busy ? t("auth.loggingIn") : t("auth.logIn")} @@ -7193,7 +7193,7 @@ index 79c8282..97731f1 100644 lang: "en", icons: [ diff --git a/app/settings.css b/app/settings.css -index f0e2e95..d284b63 100644 +index f0e2e95..547fa2a 100644 --- a/app/settings.css +++ b/app/settings.css @@ -35,14 +35,15 @@ @@ -7836,7 +7836,17 @@ index f0e2e95..d284b63 100644 outline-offset: 2px; } -@@ -1274,33 +1324,41 @@ +@@ -1266,7 +1316,8 @@ + + .config-button-success-icon { + flex-shrink: 0; +- stroke-dasharray: 18; ++ /* Lucide's check is a 22.6-unit stroke: drawn from its start. */ ++ stroke-dasharray: 23; + animation: saved-check-draw 0.35s ease forwards; + } + +@@ -1274,33 +1325,41 @@ margin-left: auto; } @@ -7890,7 +7900,7 @@ index f0e2e95..d284b63 100644 .config-switch::before { content: ""; position: absolute; -@@ -1309,7 +1367,7 @@ +@@ -1309,7 +1368,7 @@ .config-switch:disabled { cursor: default; @@ -7899,7 +7909,7 @@ index f0e2e95..d284b63 100644 } .config-switch.is-loading:disabled { -@@ -1320,55 +1378,51 @@ +@@ -1320,55 +1379,51 @@ position: absolute; top: 2px; left: 2px; @@ -7971,7 +7981,7 @@ index f0e2e95..d284b63 100644 background: transparent; color: var(--text-dim); cursor: pointer; -@@ -1384,7 +1438,7 @@ +@@ -1384,7 +1439,7 @@ .config-list-action-button[aria-current="page"] { background: var(--bg-selected); @@ -7980,7 +7990,7 @@ index f0e2e95..d284b63 100644 } .config-list-action-button:disabled { -@@ -1408,6 +1462,15 @@ +@@ -1408,6 +1463,15 @@ opacity: 0.7; } @@ -7996,7 +8006,7 @@ index f0e2e95..d284b63 100644 .settings-dialog-backdrop { position: fixed; inset: 0; -@@ -1415,7 +1478,7 @@ +@@ -1415,7 +1479,7 @@ display: flex; align-items: center; justify-content: center; @@ -8005,7 +8015,7 @@ index f0e2e95..d284b63 100644 } /* Focus moves to the dialog itself when Settings opens (tabIndex -1); it is not a control, so no ring. */ -@@ -1424,127 +1487,184 @@ +@@ -1424,127 +1488,184 @@ } .settings-dialog-surface { @@ -8261,7 +8271,7 @@ index f0e2e95..d284b63 100644 } .settings-dialog-main { -@@ -1563,194 +1683,198 @@ +@@ -1563,194 +1684,198 @@ min-height: 0; } @@ -8583,7 +8593,7 @@ index f0e2e95..d284b63 100644 font-family: var(--font-mono); font-weight: var(--font-mono-weight); } -@@ -1760,197 +1884,324 @@ +@@ -1760,197 +1885,324 @@ } .settings-font-input:focus-visible { @@ -9039,7 +9049,7 @@ index f0e2e95..d284b63 100644 } @media (max-width: 640px) { -@@ -1971,13 +2222,13 @@ +@@ -1971,13 +2223,13 @@ width: 100%; height: 190px; border-right: 0; @@ -9055,7 +9065,7 @@ index f0e2e95..d284b63 100644 } .agents-feature-actions { -@@ -1989,29 +2240,6 @@ +@@ -1989,29 +2241,6 @@ padding: 14px; } @@ -9085,7 +9095,16 @@ index f0e2e95..d284b63 100644 } @keyframes saved-pop { -@@ -2150,7 +2378,7 @@ +@@ -2021,7 +2250,7 @@ + } + + @keyframes saved-check-draw { +- from { stroke-dashoffset: 18; opacity: 0; } ++ from { stroke-dashoffset: 23; opacity: 0; } + to { stroke-dashoffset: 0; opacity: 1; } + } + +@@ -2150,7 +2379,7 @@ max-width: 100%; padding: 0 5px; border: 1px solid var(--border); @@ -9094,7 +9113,7 @@ index f0e2e95..d284b63 100644 background: var(--bg-panel); color: var(--text); font-family: var(--font-mono); -@@ -2169,7 +2397,7 @@ +@@ -2169,7 +2398,7 @@ padding: 6px 8px; overflow: auto; border: 1px solid var(--border); @@ -9103,7 +9122,7 @@ index f0e2e95..d284b63 100644 background: var(--bg-panel); color: var(--text-muted); font-family: var(--font-mono); -@@ -2256,27 +2484,14 @@ +@@ -2256,27 +2485,14 @@ min-width: 0; height: 32px; padding: 0 10px; @@ -9133,7 +9152,7 @@ index f0e2e95..d284b63 100644 .mcp-add-toggle { display: flex; -@@ -2350,10 +2565,9 @@ select.mcp-add-input { +@@ -2350,10 +2566,9 @@ select.mcp-add-input { max-width: 100%; max-height: 100%; overflow: hidden; @@ -9147,7 +9166,7 @@ index f0e2e95..d284b63 100644 } /* Focus moves to the dialog itself when it opens (tabIndex -1); it is not a control, so no ring. */ -@@ -2366,7 +2580,7 @@ select.mcp-add-input { +@@ -2366,7 +2581,7 @@ select.mcp-add-input { flex: 1 1 auto; gap: 12px; min-height: 0; @@ -9156,7 +9175,7 @@ index f0e2e95..d284b63 100644 overflow: auto; overscroll-behavior: contain; } -@@ -2399,7 +2613,7 @@ select.mcp-add-input { +@@ -2399,7 +2614,7 @@ select.mcp-add-input { margin-top: 10px; padding: 8px 10px; border: 1px solid var(--border); @@ -9165,7 +9184,7 @@ index f0e2e95..d284b63 100644 background: var(--bg); color: var(--text); font-family: var(--font-mono); -@@ -2417,7 +2631,7 @@ select.mcp-add-input { +@@ -2417,7 +2632,7 @@ select.mcp-add-input { .project-trust-error { flex-shrink: 0; max-height: 30vh; @@ -9174,7 +9193,7 @@ index f0e2e95..d284b63 100644 overflow: auto; color: #ef4444; font-size: 12px; -@@ -2437,19 +2651,20 @@ select.mcp-add-input { +@@ -2437,19 +2652,20 @@ select.mcp-add-input { flex-shrink: 0; justify-content: flex-end; gap: 8px; @@ -9202,7 +9221,7 @@ index f0e2e95..d284b63 100644 } .project-trust-button:disabled { -@@ -2457,10 +2672,8 @@ select.mcp-add-input { +@@ -2457,10 +2673,8 @@ select.mcp-add-input { } .project-trust-button.is-primary { @@ -9215,7 +9234,7 @@ index f0e2e95..d284b63 100644 } .project-trust-button.is-primary:disabled { -@@ -2534,7 +2747,7 @@ select.mcp-add-input { +@@ -2534,7 +2748,7 @@ select.mcp-add-input { min-width: 0; padding: 8px 10px; border: 1px solid var(--border); @@ -9224,7 +9243,7 @@ index f0e2e95..d284b63 100644 background: var(--bg); } -@@ -2556,7 +2769,7 @@ select.mcp-add-input { +@@ -2556,7 +2770,7 @@ select.mcp-add-input { .project-trust-mcp-tag { flex-shrink: 0; padding: 1px 5px; @@ -9233,7 +9252,7 @@ index f0e2e95..d284b63 100644 background: rgba(120, 120, 120, 0.12); color: var(--text-dim); font-size: 10px; -@@ -2577,7 +2790,7 @@ select.mcp-add-input { +@@ -2577,7 +2791,7 @@ select.mcp-add-input { display: inline-block; margin: 2px 4px 0 0; padding: 0 4px; @@ -10778,7 +10797,7 @@ index 737e7a5..fdf88a2 100644 .sidebar-sheet .sidebar-menu-question-text { diff --git a/app/sidebar.css b/app/sidebar.css -index 0b8f66a..262d991 100644 +index 0b8f66a..27eaf4f 100644 --- a/app/sidebar.css +++ b/app/sidebar.css @@ -91,8 +91,9 @@ @@ -11386,7 +11405,7 @@ index 0b8f66a..262d991 100644 min-width: 0; } -+/* Spaced by ink, not boxes: Carbon's plus draws the middle 6px of its 12px box, and "N" ++/* Spaced by ink, not boxes: Lucide's plus draws the middle 7px of its 12px box, and "N" + sits 1.5px into its own. The plus's ink starts about 8.6px from the left, as the label's + ends from the right (within a device pixel at 4x), and they are 6.5px apart: our + buttons' icon-to-label space (the composer's mode button). */ @@ -11579,7 +11598,7 @@ index 0b8f66a..262d991 100644 .sidebar-tool-button.is-active, diff --git a/app/ui.css b/app/ui.css new file mode 100644 -index 0000000..dd0300a +index 0000000..2dfbbbd --- /dev/null +++ b/app/ui.css @@ -0,0 +1,971 @@ @@ -12522,10 +12541,10 @@ index 0000000..dd0300a +.directory-picker-new { margin-right: auto; padding-left: var(--popup-row-pad); } + +/* ---------------------------------------------------------------- selects -+ components/ui/select.tsx: a native select with Carbon's 12px chevron instead of the ++ components/ui/select.tsx: a native select with Lucide's 12px chevron instead of the + engine's arrow (Chromium's sat 3px from the edge whatever the padding, after 4.5px of + its own before the text; Firefox and Safari placed theirs elsewhere). The chevron's ink -+ mirrors the text's padding (--select-pad; its ink starts 2.25px inside its box, the ++ mirrors the text's padding (--select-pad; its ink starts 3px inside its box, the + text's 0.75px past the padding) and starts 6.25px after the text, as in the menu triggers. + The select keeps its own colours and border; the padding is set by the component. */ +.ui-select { @@ -12555,7 +12574,7 @@ index 0000000..dd0300a +} +.ui-select:has(> select:disabled) > .ui-select-chevron { opacity: 0.45; } diff --git a/components/AgentSessionPanel.tsx b/components/AgentSessionPanel.tsx -index 8d51f3c..61fcf9e 100644 +index 8d51f3c..6655b11 100644 --- a/components/AgentSessionPanel.tsx +++ b/components/AgentSessionPanel.tsx @@ -2,9 +2,12 @@ @@ -12566,8 +12585,8 @@ index 8d51f3c..61fcf9e 100644 import type { SessionUsageSummary } from "@/lib/session-stats"; import { formatActiveDuration, formatCompactCount, formatCostShort } from "@/lib/session-stats-format"; import type { SessionInfo, SubagentSessionStatus } from "@/lib/types"; -+import { CarbonIcon } from "./CarbonIcon"; -+import * as Carbon from "./carbon/icons"; ++import { LucideIcon } from "./LucideIcon"; ++import * as Lucide from "./lucide/icons"; interface Props { rootSession: SessionInfo; @@ -12578,18 +12597,27 @@ index 8d51f3c..61fcf9e 100644 - -+ ++ {formatActiveDuration(usage.totalActiveMs)} )} -@@ -144,22 +145,16 @@ export function StatusIcon({ status }: { status: SubagentSessionStatus }) { +@@ -136,30 +137,21 @@ export function statusColor(status: SubagentSessionStatus): string { + export function StatusIcon({ status }: { status: SubagentSessionStatus }) { + if (status === "running" || status === "starting") { + return ( +- ++ + ); } if (status === "failed") { return ( - -+ ++ ); } if (status === "aborted" || status === "interrupted") { @@ -12597,34 +12625,34 @@ index 8d51f3c..61fcf9e 100644 - -+ ++ ); } return ( - -+ ++ ); } -@@ -228,13 +223,9 @@ function AgentRow({ +@@ -228,13 +220,9 @@ function AgentRow({ > {main ? ( - -+ ++ ) : ( - -+ ++ )} -@@ -268,10 +259,7 @@ function AgentRow({ +@@ -268,10 +256,7 @@ function AgentRow({ onMouseEnter={(event) => { event.currentTarget.style.background = "var(--bg-hover)"; if (!tabShown) event.currentTarget.style.color = "var(--text)"; }} onMouseLeave={(event) => { event.currentTarget.style.background = "none"; if (!tabShown) event.currentTarget.style.color = "var(--text-muted)"; }} > @@ -12632,11 +12660,11 @@ index 8d51f3c..61fcf9e 100644 - - {tabShown && } - -+ ++ )} -@@ -295,7 +283,7 @@ export function AgentSessionPanel({ rootSession, subagents, selectedSessionId, r +@@ -295,7 +280,7 @@ export function AgentSessionPanel({ rootSession, subagents, selectedSessionId, r .some((value) => value?.toLowerCase().includes(normalizedQuery)); }) : sortedSubagents; @@ -12645,7 +12673,7 @@ index 8d51f3c..61fcf9e 100644 const familyIds = useMemo(() => [rootSession.id, ...subagents.map((session) => session.id)], [rootSession.id, subagents]); const runningKey = familyIds.filter((id) => runningSessionIds.has(id)).join(","); const usage = useAgentUsage(familyIds, runningKey); -@@ -325,15 +313,8 @@ export function AgentSessionPanel({ rootSession, subagents, selectedSessionId, r +@@ -325,15 +310,8 @@ export function AgentSessionPanel({ rootSession, subagents, selectedSessionId, r
-@@ -365,7 +346,7 @@ export function AgentSessionPanel({ rootSession, subagents, selectedSessionId, r +@@ -365,7 +343,7 @@ export function AgentSessionPanel({ rootSession, subagents, selectedSessionId, r aria-label={t("agentSwitcher.search")} style={{ width: "100%", height: 32, padding: "0 10px", @@ -12673,15 +12701,15 @@ index 8d51f3c..61fcf9e 100644 }} /> diff --git a/components/AgentsConfig.tsx b/components/AgentsConfig.tsx -index 861a1c6..efd7e87 100644 +index 861a1c6..a0b4c72 100644 --- a/components/AgentsConfig.tsx +++ b/components/AgentsConfig.tsx @@ -38,6 +38,9 @@ import { } from "./SettingsUi"; import { ModelSelector } from "./ModelSelector"; import { projectTrustReloadKey } from "./settings-ui-helpers"; -+import { CarbonIcon } from "./CarbonIcon"; -+import * as Carbon from "./carbon/icons"; ++import { LucideIcon } from "./LucideIcon"; ++import * as Lucide from "./lucide/icons"; +import { NativeSelect } from "./ui/select"; const TOOL_OPTIONS = ["read", "bash", "edit", "write", "grep", "find", "ls"]; @@ -12738,7 +12766,7 @@ index 861a1c6..efd7e87 100644 - - - -+ ++ )} {savedOk ? t("i18n.saved") : saving ? t("agents.saving") : t("agents.save")} @@ -12775,7 +12803,7 @@ index e232a55..0000000 - assert.deepEqual(saved, { tabs: [{ ...tab, restored: true }], activeId: null, open: false }); -}); diff --git a/components/AppShell.tsx b/components/AppShell.tsx -index bcc3807..3648bf4 100644 +index bcc3807..1a8346e 100644 --- a/components/AppShell.tsx +++ b/components/AppShell.tsx @@ -3,6 +3,8 @@ @@ -12854,8 +12882,8 @@ index bcc3807..3648bf4 100644 -import { getSessionFamily } from "@/lib/session-family"; +import { getAgentsBadge, getSessionFamily } from "@/lib/session-family"; import { getLastSettingsSection, settingsSectionRequiresProject, type SettingsSection } from "@/lib/settings-navigation"; -+import { CarbonIcon } from "./CarbonIcon"; -+import * as Carbon from "./carbon/icons"; ++import { LucideIcon } from "./LucideIcon"; ++import * as Lucide from "./lucide/icons"; +import { ChangesPane } from "./ChangesPane"; +import { BrowserPane } from "./BrowserPane"; +import { PANES, SessionHeader, type DiffSummary, type SessionMenuAction } from "./SessionHeader"; @@ -13623,7 +13651,7 @@ index bcc3807..3648bf4 100644 + // out: the switch (in a checkout), a folder of its own (anything else). + const newSessionNotice = !startsInWorktree && chatsInNewSessionFolder.length > 0 && folderShareKept !== effectiveNewSessionCwd ? ( +
-+ ++ + + {chatsInNewSessionFolder.length === 1 + ? translate("folderShare.one", { title: shortTitle(sessionHeaderTitle(chatsInNewSessionFolder[0])) }) @@ -13681,17 +13709,17 @@ index bcc3807..3648bf4 100644 + { + id: "name", + label: autoNameStatus.kind === "naming" ? translate("title.generating") : translate("title.generate"), -+ icon: Carbon.MagicWand, ++ icon: Lucide.WandSparkles, + onSelect: () => { void handleAutoName(); }, + disabled: selectedSession.transient || !titleHasMessages || autoNameStatus.kind === "naming", + }, -+ { id: "details", label: translate("session.title"), icon: Carbon.Meter, onSelect: openSessionStatsPanel }, -+ ...(isMobile && sessionHasBranches ? [{ id: "branches", label: translate("i18n.branches"), icon: Carbon.Branch, onSelect: () => toggleTopPanel("branches") }] : []), -+ { id: "history", label: translate("history.full"), icon: Carbon.RecentlyViewed, onSelect: handleViewFullHistory }, ++ { id: "details", label: translate("session.title"), icon: Lucide.Gauge, onSelect: openSessionStatsPanel }, ++ ...(isMobile && sessionHasBranches ? [{ id: "branches", label: translate("i18n.branches"), icon: Lucide.GitBranch, onSelect: () => toggleTopPanel("branches") }] : []), ++ { id: "history", label: translate("history.full"), icon: Lucide.RotateCcwClock, onSelect: handleViewFullHistory }, + ] : []), + ...(showChat ? [ -+ { id: "system", label: translate("system.prompt"), icon: Carbon.Document, onSelect: () => handleSystemInfoToggle("system") }, -+ { id: "tools", label: translate("tools.title"), icon: Carbon.Tools, onSelect: () => handleSystemInfoToggle("tools") }, ++ { id: "system", label: translate("system.prompt"), icon: Lucide.File, onSelect: () => handleSystemInfoToggle("system") }, ++ { id: "tools", label: translate("tools.title"), icon: Lucide.Wrench, onSelect: () => handleSystemInfoToggle("tools") }, + ] : []), + ]; + @@ -13780,7 +13808,7 @@ index bcc3807..3648bf4 100644 - - - -+ ++ {translate("trust.resourcesNotLoaded")} ); @@ -14130,7 +14158,7 @@ index bcc3807..3648bf4 100644 - - - -+ ++ - ) : ( - -+ ++ + {/* While subagents run, how many; otherwise all of them. */} + {agentsBadge?.count ?? activeSessionFamily!.subagents.length} + @@ -14735,13 +14763,13 @@ index bcc3807..3648bf4 100644 - -+ ++ ) : ( - -+ ++ )} ); @@ -14845,7 +14873,7 @@ index bcc3807..3648bf4 100644 - - - -+ ++
{translate("workspace.getStarted")}
@@ -15181,15 +15209,15 @@ index c2e7080..9d4e9da 100644 + assert.equal(frames.length, 0); +}); diff --git a/components/BranchNavigator.tsx b/components/BranchNavigator.tsx -index 0f8186b..ef6cc22 100644 +index 0f8186b..420eb4c 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"; ++import { LucideIcon } from "./LucideIcon"; ++import * as Lucide from "./lucide/icons"; interface Props { tree: SessionTreeNode[]; @@ -15216,14 +15244,14 @@ index 0f8186b..ef6cc22 100644 - - - -+ ++ ); const chevron = ( - - - -+ ++ ); @@ -15248,7 +15276,7 @@ index 0f8186b..ef6cc22 100644
diff --git a/components/BrowserPane.tsx b/components/BrowserPane.tsx new file mode 100644 -index 0000000..26533b4 +index 0000000..5b49eb8 --- /dev/null +++ b/components/BrowserPane.tsx @@ -0,0 +1,809 @@ @@ -15285,8 +15313,8 @@ index 0000000..26533b4 +} from "@/lib/browser/types"; +import { BROWSER_VIEWPORT_LIMITS } from "@/lib/browser/types"; +import { PREVIEW_LOAD_TIMEOUT_MS, previewProblemOf, type PreviewProblem } from "@/lib/browser/preview-check"; -+import { CarbonIcon } from "./CarbonIcon"; -+import * as Carbon from "./carbon/icons"; ++import { LucideIcon } from "./LucideIcon"; ++import * as Lucide from "./lucide/icons"; +import { MenuItem, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuRoot, MenuSection, MenuSeparator, MenuSubmenu, MenuTrigger } from "./ui/menu"; +import { Tip } from "./ui/tooltip"; +import { shortcutLabel } from "@/lib/shortcuts"; @@ -15762,7 +15790,7 @@ index 0000000..26533b4 + if (state && !state.available) { + return ( +
-+ ++ + {t("browser.unavailable")} + {state.reason} +
@@ -15774,12 +15802,12 @@ index 0000000..26533b4 +
+ + + + + + + @@ -15790,7 +15818,7 @@ index 0000000..26533b4 + disabled={!ready} + onClick={() => void act({ action: activeTab?.loading ? "stop" : "reload" })} + > -+ ++ + + +
+ + @@ -15844,7 +15872,7 @@ index 0000000..26533b4 + disabled={!ready || previewing} + onClick={() => setPicking((on) => !on)} + > -+ ++ + + + @@ -15855,19 +15883,19 @@ index 0000000..26533b4 + aria-pressed={consoleOpen} + onClick={() => setConsoleOpen((open) => !open)} + > -+ ++ + {errors > 0 && {errors}} + + + + + -+ ++ + + + -+ void act({ action: "new_tab", url: "about:blank" })}>{t("browser.newTab")} -+ ++ void act({ action: "new_tab", url: "about:blank" })}>{t("browser.newTab")} ++ + {portChips.length === 0 && {t("browser.noDevServers")}} + {portChips.map((port) => ( + navigate(port.url)} hint={port.process}> @@ -15885,15 +15913,15 @@ index 0000000..26533b4 + try { window.localStorage.setItem(DEVICE_KEY, next); } catch { /* storage is optional */ } + }} + > -+ {t("browser.sizeFit")} -+ {t("browser.sizeDesktop")} -+ {t("browser.sizeTablet")} -+ {t("browser.sizePhone")} ++ {t("browser.sizeFit")} ++ {t("browser.sizeDesktop")} ++ {t("browser.sizeTablet")} ++ {t("browser.sizePhone")} + + + -+ void act({ action: "clear_data" })} disabled={!state?.running}>{t("browser.clearData")} -+ void act({ action: "shutdown" })} disabled={!state?.running}>{t("browser.shutdown")} ++ void act({ action: "clear_data" })} disabled={!state?.running}>{t("browser.clearData")} ++ void act({ action: "shutdown" })} disabled={!state?.running}>{t("browser.shutdown")} + + +
@@ -15912,7 +15940,7 @@ index 0000000..26533b4 + void act({ action: "close_tab", tabId: tab.id }); + }} + > -+ ++ + +
+ ))} @@ -15928,22 +15956,22 @@ index 0000000..26533b4 + + {preview && ( +
-+ ++ + {t("browser.previewOf", { host: preview.host })} + {t("browser.previewNote")} + + + + + -+ ++ + + + + + +
@@ -15969,7 +15997,7 @@ index 0000000..26533b4 + />} + {previewProblem && ( +
-+ ++ + {t("browser.previewFailed")} + + {previewProblem.status === null ? t("browser.previewFailedTimeout") : t("browser.previewFailedStatus", { status: previewProblem.status })} @@ -16021,7 +16049,7 @@ index 0000000..26533b4 +
+ ) : ( +
-+ ++ + {t("browser.emptyTitle")} + {t("browser.emptyBody")} + {portChips.length > 0 && ( @@ -16061,50 +16089,6 @@ index 0000000..26533b4 +
+ ); +} -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/ChangesPane.test.mjs b/components/ChangesPane.test.mjs new file mode 100644 index 0000000..5ed7bb3 @@ -16170,7 +16154,7 @@ index 0000000..5ed7bb3 +}); diff --git a/components/ChangesPane.tsx b/components/ChangesPane.tsx new file mode 100644 -index 0000000..05fa3d7 +index 0000000..911db8c --- /dev/null +++ b/components/ChangesPane.tsx @@ -0,0 +1,421 @@ @@ -16189,8 +16173,8 @@ index 0000000..05fa3d7 +import { useI18n } from "@/hooks/useI18n"; +import { useTheme } from "@/hooks/useTheme"; +import type { GitFileDiffResponse, GitFileStatus, GitStatusResponse } from "@/lib/git-types"; -+import { CarbonIcon } from "./CarbonIcon"; -+import * as Carbon from "./carbon/icons"; ++import { LucideIcon } from "./LucideIcon"; ++import * as Lucide from "./lucide/icons"; +import { getFileIcon } from "./FileIcons"; +import { RENDER, UNSAFE_CSS, VIRTUAL_METRICS, codePool, hashText, keyboardExpanders } from "./highlighting"; +import { Tip } from "./ui/tooltip"; @@ -16471,7 +16455,7 @@ index 0000000..05fa3d7 + const { comment } = data; + return ( +
-+ ++ +
{comment.text}
+ + + +
@@ -16510,7 +16494,7 @@ index 0000000..05fa3d7 + + + + + @@ -16520,7 +16504,7 @@ index 0000000..05fa3d7 + disabled={commentCount === 0} + onClick={send} + > -+ ++ + {commentCount > 0 ? t("changes.sendCount", { count: commentCount }) : t("changes.send")} + + @@ -16880,7 +16864,7 @@ index 9189877..29ac1b6 100644 + assert.match(plain, /<\/span>/); }); diff --git a/components/ChatInput.tsx b/components/ChatInput.tsx -index 9a0a1b7..d23c41b 100644 +index 9a0a1b7..018c5b6 100644 --- a/components/ChatInput.tsx +++ b/components/ChatInput.tsx @@ -1,8 +1,9 @@ @@ -16907,8 +16891,8 @@ index 9a0a1b7..d23c41b 100644 +import { ContextMeter } from "./ContextMeter"; +import { ComposerModelMenu } from "./ComposerModelMenu"; +import { ComposerModeMenu } from "./ComposerModeMenu"; -+import { CarbonIcon } from "./CarbonIcon"; -+import * as Carbon from "./carbon/icons"; ++import { LucideIcon } from "./LucideIcon"; ++import * as Lucide from "./lucide/icons"; export { filterModelOptions } from "./ModelSelector"; @@ -17068,11 +17052,11 @@ index 9a0a1b7..d23c41b 100644 +// One icon column in the list, as the reference's: each command shows what it is, +// with the "+" menu's icons (commands, connectors, skills) and a page for a prompt. -+const SLASH_SOURCE_ICONS: Record = { -+ builtin: Carbon.Terminal, -+ extension: Carbon.Plug, -+ prompt: Carbon.Document, -+ skill: Carbon.Layers, ++const SLASH_SOURCE_ICONS: Record = { ++ builtin: Lucide.Terminal, ++ extension: Lucide.Plug, ++ prompt: Lucide.File, ++ skill: Lucide.Layers, +}; + const SLASH_SOURCE_ORDER: Record = { @@ -17106,7 +17090,7 @@ index 9a0a1b7..d23c41b 100644 - - - -+ ++
{title}
{body}
@@ -17514,7 +17498,7 @@ index 9a0a1b7..d23c41b 100644 + if (listMode === "history") { + return [{ + key: "history", -+ items: inputHistory.map((item, index) => ({ key: `${index}:${item}`, icon: , label: item })), ++ items: inputHistory.map((item, index) => ({ key: `${index}:${item}`, icon: , label: item })), + }]; + } + if (listMode === "slash") { @@ -17526,7 +17510,7 @@ index 9a0a1b7..d23c41b 100644 + const description = getSlashDescription(command, t); + return { + key: `${command.source}:${command.name}`, -+ icon: , ++ icon: , + label: /{command.name}, + description: description || undefined, + meta: dormant ? {t("chat.dormant")} : undefined, @@ -17647,7 +17631,7 @@ index 9a0a1b7..d23c41b 100644 + + ) : ( + + )} +
@@ -17799,7 +17783,7 @@ index 9a0a1b7..d23c41b 100644 - - {t("chat.retrying", { attempt: retryInfo.attempt, max: retryInfo.maxAttempts })}{retryInfo.errorMessage && — {retryInfo.errorMessage}} +
-+ ++ + + {t("chat.retrying", { attempt: retryInfo.attempt, max: retryInfo.maxAttempts })} + {retryInfo.errorMessage && — {retryInfo.errorMessage}} @@ -17818,7 +17802,7 @@ index 9a0a1b7..d23c41b 100644 - - {compactResultText} +
-+ ++ + {compactResultText}
)} @@ -17884,7 +17868,7 @@ index 9a0a1b7..d23c41b 100644 + {onRecallQueue && ( + + + @@ -18143,7 +18127,7 @@ index 9a0a1b7..d23c41b 100644 + + + +
+ ))} @@ -18689,32 +18673,32 @@ index 9a0a1b7..d23c41b 100644 + + + 0 || undefined}> -+ ++ + + + -+ fileInputRef.current?.click()}> ++ fileInputRef.current?.click()}> + {onAttachFiles ? t("chat.attachFilesOrPhotos") : t("chat.attachPhotos")} + + {cwd && ( -+ insertAtCaret("@")} hint="@"> ++ insertAtCaret("@")} hint="@"> + {t("chat.mentionFile")} + + )} + -+ insertAtCaret("/", true)} hint="/"> ++ insertAtCaret("/", true)} hint="/"> + {t("chat.commandsItem")} + -+ insertAtCaret("/skill:", true)}> ++ insertAtCaret("/skill:", true)}> + {t("chat.skillsItem")} + -+ insertAtCaret("!", true)} hint="!"> ++ insertAtCaret("!", true)} hint="!"> + {t("chat.shellItem")} + + {onOpenSettings && ( + <> + -+ onOpenSettings("mcp")}> ++ onOpenSettings("mcp")}> + {t("chat.connectorsItem")} + + @@ -18965,14 +18949,14 @@ index 9a0a1b7..d23c41b 100644 + {onSteer && canQueueStreamingMessage && ( + + + + )} + {/* Stop takes Send's place, as in Claude. */} + + + + @@ -18992,7 +18976,7 @@ index 9a0a1b7..d23c41b 100644 + title={t("chat.send")} + aria-label={t("chat.send")} + > -+ ++ + )} - @@ -20381,7 +20365,7 @@ index 0000000..37707f9 + } +}); diff --git a/components/ChatWindow.tsx b/components/ChatWindow.tsx -index ef7a23f..acb7e72 100644 +index ef7a23f..5285c5e 100644 --- a/components/ChatWindow.tsx +++ b/components/ChatWindow.tsx @@ -1,23 +1,21 @@ @@ -20432,8 +20416,8 @@ index ef7a23f..acb7e72 100644 restoreScrollTop, VISIBLE_PAGE_SIZE, } from "@/lib/chat-lazy-load"; -+import { CarbonIcon } from "./CarbonIcon"; -+import * as Carbon from "./carbon/icons"; ++import { LucideIcon } from "./LucideIcon"; ++import * as Lucide from "./lucide/icons"; interface Props { session: SessionInfo | null; @@ -20605,7 +20589,7 @@ index ef7a23f..acb7e72 100644 - {parts.join(" · ")} - {cacheMissText && {` · ${cacheMissText}`}} -
-+ ++ + {summary}{cacheMissText && {` · ${cacheMissText}`}} - {(expanded || reveal) && ( @@ -20759,7 +20743,7 @@ index ef7a23f..acb7e72 100644 - - - -+ ++
)} @@ -20944,7 +20928,7 @@ index ef7a23f..acb7e72 100644 {t("chat.askInNewChat")}
)} @@ -21038,7 +21022,7 @@ index ef7a23f..acb7e72 100644 - ? - : } - -+ ++ ); } @@ -21098,7 +21082,7 @@ index ef7a23f..acb7e72 100644 - -+ ++
@@ -21190,7 +21174,7 @@ index ef7a23f..acb7e72 100644 - -+ ++
+ ); +} +diff --git a/components/CodemodeToolView.tsx b/components/CodemodeToolView.tsx +index 51fa7fa..010e439 100644 +--- a/components/CodemodeToolView.tsx ++++ b/components/CodemodeToolView.tsx +@@ -2,6 +2,8 @@ + + import { useState } from "react"; + import { useI18n } from "@/hooks/useI18n"; ++import { LucideIcon } from "./LucideIcon"; ++import * as Lucide from "./lucide/icons"; + import { + codemodeTotalCost, + formatCodemodeCost, +@@ -17,11 +19,11 @@ import { + /** Calls shown before the rest are folded behind a button; the newest stay visible. */ + export const CODEMODE_VISIBLE_CALLS = 20; + +-const STATUS_STYLE: Record = { +- running: { icon: "…", color: "#d97706" }, +- ok: { icon: "✓", color: "#16a34a" }, +- error: { icon: "✗", color: "#f87171" }, +- cancelled: { icon: "⊘", color: "var(--text-dim)" }, ++const STATUS_STYLE: Record = { ++ running: { icon: Lucide.Ellipsis, color: "#d97706" }, ++ ok: { icon: Lucide.Check, color: "#16a34a" }, ++ error: { icon: Lucide.X, color: "#f87171" }, ++ cancelled: { icon: Lucide.Ban, color: "var(--text-dim)" }, + }; + + function CallRow({ call }: { call: CodemodeCallView }) { +@@ -35,9 +37,9 @@ function CallRow({ call }: { call: CodemodeCallView }) { + role="img" + aria-label={t(`codemode.status.${call.status}`)} + title={t(`codemode.status.${call.status}`)} +- style={{ color: status.color, width: 12, flexShrink: 0, textAlign: "center" }} ++ style={{ color: status.color, width: 12, flexShrink: 0, alignSelf: "center", display: "flex" }} + > +- {status.icon} ++ + + {call.name} + {call.args && ( diff --git a/components/ComposerModeMenu.tsx b/components/ComposerModeMenu.tsx new file mode 100644 -index 0000000..0220dc0 +index 0000000..e0bb3ae --- /dev/null +++ b/components/ComposerModeMenu.tsx @@ -0,0 +1,96 @@ @@ -21559,14 +21585,14 @@ index 0000000..0220dc0 +import { useI18n } from "@/hooks/useI18n"; +import type { ToolPreset } from "@/lib/tool-presets"; +import { isShortcut, shortcutLabel } from "@/lib/shortcuts"; -+import { CarbonIcon } from "./CarbonIcon"; -+import * as Carbon from "./carbon/icons"; ++import { LucideIcon } from "./LucideIcon"; ++import * as Lucide from "./lucide/icons"; +import { MenuSection, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuRoot, MenuSeparator, MenuTrigger } from "./ui/menu"; +import { Tip } from "./ui/tooltip"; + +interface ModeDef { + preset: ToolPreset; -+ icon: Carbon.CarbonIconData; ++ icon: Lucide.IconData; + label: string; + description: string; +} @@ -21574,13 +21600,13 @@ index 0000000..0220dc0 +export function modeDefs(t: (key: string) => string): { main: ModeDef[]; pinned: ModeDef[] } { + return { + main: [ -+ { preset: "configured", icon: Carbon.Edit, label: t("mode.standard"), description: t("mode.standardDescription") }, -+ { preset: "read-only", icon: Carbon.ListChecked, label: t("mode.plan"), description: t("mode.planDescription") }, -+ { preset: "none", icon: Carbon.Chat, label: t("mode.chat"), description: t("mode.chatDescription") }, ++ { preset: "configured", icon: Lucide.Pencil, label: t("mode.standard"), description: t("mode.standardDescription") }, ++ { preset: "read-only", icon: Lucide.ListChecks, label: t("mode.plan"), description: t("mode.planDescription") }, ++ { preset: "none", icon: Lucide.MessageSquare, label: t("mode.chat"), description: t("mode.chatDescription") }, + ], + pinned: [ -+ { preset: "default", icon: Carbon.Code, label: t("mode.core"), description: t("mode.coreDescription") }, -+ { preset: "full", icon: Carbon.Flash, label: t("mode.all"), description: t("mode.allDescription") }, ++ { preset: "default", icon: Lucide.Code, label: t("mode.core"), description: t("mode.coreDescription") }, ++ { preset: "full", icon: Lucide.Zap, label: t("mode.all"), description: t("mode.allDescription") }, + ], + }; +} @@ -21616,9 +21642,9 @@ index 0000000..0220dc0 + + + -+ ++ + {!compact && {current.label}} -+ {!compact && } ++ {!compact && } + + + {/* Wide enough for each description on its row, after the mode's name. */} @@ -21644,10 +21670,10 @@ index 0000000..0220dc0 +} diff --git a/components/ComposerModelMenu.tsx b/components/ComposerModelMenu.tsx new file mode 100644 -index 0000000..ba55937 +index 0000000..fc10aba --- /dev/null +++ b/components/ComposerModelMenu.tsx -@@ -0,0 +1,192 @@ +@@ -0,0 +1,190 @@ +"use client"; + +// One menu for what answers and how hard it thinks, as Claude Desktop's model @@ -21657,8 +21683,8 @@ index 0000000..ba55937 +// buttons of the old toolbar (ModelSelector stays for Settings' fields). + +import { useI18n } from "@/hooks/useI18n"; -+import { CarbonIcon } from "./CarbonIcon"; -+import * as Carbon from "./carbon/icons"; ++import { LucideIcon } from "./LucideIcon"; ++import * as Lucide from "./lucide/icons"; +import type { ModelSelectorOption } from "./ModelSelector"; +import { ModelMark } from "./ModelMark"; +import { @@ -21763,12 +21789,10 @@ index 0000000..ba55937 + title={busy ? t("chat.switchingModel") : noModels ? t("chat.noAvailableModels") : undefined} + > + {busy && ( -+ ++ + )} + {trigger} -+ ++ + + + @@ -21826,7 +21850,7 @@ index 0000000..ba55937 + <> + + { + if (onSetDefault && value && !isDefaultModel) onSetDefault(value.provider, value.modelId); + if (onSetDefaultThinkingLevel && levelValue !== "auto" && !isDefaultLevel) onSetDefaultThinkingLevel(levelValue); @@ -21971,7 +21995,7 @@ index 0000000..abee6e7 + ); +} diff --git a/components/DirectoryPicker.tsx b/components/DirectoryPicker.tsx -index 433cd89..f338804 100644 +index 433cd89..ff46938 100644 --- a/components/DirectoryPicker.tsx +++ b/components/DirectoryPicker.tsx @@ -1,8 +1,18 @@ @@ -21981,8 +22005,8 @@ index 433cd89..f338804 100644 +import { type FormEvent, type KeyboardEvent, useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { useI18n } from "@/hooks/useI18n"; -+import { CarbonIcon } from "./CarbonIcon"; -+import * as Carbon from "./carbon/icons"; ++import { LucideIcon } from "./LucideIcon"; ++import * as Lucide from "./lucide/icons"; +import { FolderIcon } from "./FileIcons"; +import { keepTabInside } from "@/lib/focus"; + @@ -22165,7 +22189,7 @@ index 433cd89..f338804 100644 - style={{ padding: "2px 6px", border: 0, background: "none", color: "var(--text-muted)", fontSize: 20, lineHeight: 1, cursor: busy ? "default" : "pointer", opacity: busy ? 0.5 : 1 }} > - × -+ ++
@@ -22183,7 +22207,7 @@ index 433cd89..f338804 100644 + title={t("directoryPicker.goToParent")} + aria-label={t("directoryPicker.goToParent")} + > -+ ++ -