feat(ui): add dynamic window title and sprite-based project/file icons (#529)

* feat(ui): add dynamic titles and sprite-based project/file icons

* feat(files): add viewer syntax fallback and tab file icons

* fix(files): restore file viewer highlighting and add diff file icons

* feat(git): add file icons and async file-viewer syntax fallback

* fix(files): force codemirror token colors in file viewer

* feat(files): add shiki view mode for file viewer

* fix(files): force codemirror parse after programmatic content updates

* feat(files): support markdown frontmatter preview

* feat(chat): use pierre diffs for tool previews

* feat(chat): add configurable beautiful-mermaid rendering

* feat(perf): virtualize chat rendering and add react-scan toggle

* feat(build): enable React Compiler in Vite React apps

* fix(chat): reduce rerenders from tooltips and streamed activity

* fix(ui): make MessageList React Compiler safe

* chore(ui): batch commit remaining pending ui updates

* fix: polish chat and diff preview rendering

- Keep Mermaid action buttons fixed while diagram content scrolls
- Align Diff All Files headers and match Git-style path truncation
- Default chat tool diffs to unified view with lightweight indicators disabled

* fix: preserve file tree expansion and delay git action label collapse

* fix: refine project icon controls in settings

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
shekohex
2026-02-27 20:03:42 +02:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent d6b8f28e6f
commit 1d8ff97c95
1134 changed files with 14091 additions and 2005 deletions
@@ -9,10 +9,10 @@ import { cn, formatDirectoryName } from '@/lib/utils';
import { getAgentColor } from '@/lib/agentColors';
import { RiLoader4Line, RiAddLine } from '@remixicon/react';
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP } from '@/lib/projectMeta';
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { toast } from '@/components/ui';
import { isTauriShell, isDesktopLocalOriginActive } from '@/lib/desktop';
import { isTauriShell, isDesktopLocalOriginActive, requestDirectoryAccess } from '@/lib/desktop';
import { sessionEvents } from '@/lib/sessionEvents';
import {
Dialog,
@@ -184,72 +184,61 @@ function useProjectStatus(
const sessionsByDirectory = useSessionStore((state) => state.sessionsByDirectory);
const getSessionsByDirectory = useSessionStore((state) => state.getSessionsByDirectory);
const projectStatusMap = React.useMemo(() => {
const result = new Map<string, { hasRunning: boolean; hasUnread: boolean }>();
const projectStatusMap = React.useCallback((projectPath: string): { hasRunning: boolean; hasUnread: boolean } => {
const getStatusType = (sessionId: string): 'busy' | 'retry' | 'idle' => {
const status = sessionStatus?.get(sessionId);
if (status?.type === 'busy' || status?.type === 'retry') return status.type;
return 'idle';
};
return (projectPath: string): { hasRunning: boolean; hasUnread: boolean } => {
const cached = result.get(projectPath);
if (cached) return cached;
const projectRoot = normalize(projectPath);
if (!projectRoot) {
return { hasRunning: false, hasUnread: false };
}
const projectRoot = normalize(projectPath);
if (!projectRoot) {
const empty = { hasRunning: false, hasUnread: false };
result.set(projectPath, empty);
return empty;
}
const dirs: string[] = [projectRoot];
const worktrees = availableWorktreesByProject.get(projectRoot) ?? [];
for (const meta of worktrees) {
const p = (meta && typeof meta === 'object' && 'path' in meta) ? (meta as { path?: unknown }).path : null;
if (typeof p === 'string' && p.trim()) {
const normalized = normalize(p);
if (normalized && normalized !== projectRoot) {
dirs.push(normalized);
}
const dirs: string[] = [projectRoot];
const worktrees = availableWorktreesByProject.get(projectRoot) ?? [];
for (const meta of worktrees) {
const p = (meta && typeof meta === 'object' && 'path' in meta) ? (meta as { path?: unknown }).path : null;
if (typeof p === 'string' && p.trim()) {
const normalized = normalize(p);
if (normalized && normalized !== projectRoot) {
dirs.push(normalized);
}
}
}
const seen = new Set<string>();
let hasRunning = false;
let hasUnread = false;
const seen = new Set<string>();
let hasRunning = false;
let hasUnread = false;
for (const dir of dirs) {
const list = sessionsByDirectory.get(dir) ?? getSessionsByDirectory(dir);
for (const session of list) {
if (!session?.id || seen.has(session.id)) {
continue;
}
seen.add(session.id);
const statusType = getStatusType(session.id);
if (statusType === 'busy' || statusType === 'retry') {
hasRunning = true;
}
if (session.id !== currentSessionId && sessionAttentionStates?.get(session.id)?.needsAttention === true) {
hasUnread = true;
}
if (hasRunning && hasUnread) {
break;
}
for (const dir of dirs) {
const list = sessionsByDirectory.get(dir) ?? getSessionsByDirectory(dir);
for (const session of list) {
if (!session?.id || seen.has(session.id)) {
continue;
}
seen.add(session.id);
const statusType = getStatusType(session.id);
if (statusType === 'busy' || statusType === 'retry') {
hasRunning = true;
}
if (session.id !== currentSessionId && sessionAttentionStates?.get(session.id)?.needsAttention === true) {
hasUnread = true;
}
if (hasRunning && hasUnread) {
break;
}
}
if (hasRunning && hasUnread) {
break;
}
}
const status = { hasRunning, hasUnread };
result.set(projectPath, status);
return status;
};
return { hasRunning, hasUnread };
}, [sessionsByDirectory, getSessionsByDirectory, availableWorktreesByProject, sessionStatus, sessionAttentionStates, currentSessionId]);
return projectStatusMap;
@@ -390,6 +379,8 @@ interface SessionStatusHeaderProps {
currentSessionTitle: string;
currentProjectLabel?: string;
currentProjectIcon?: string | null;
currentProjectIconImageUrl?: string | null;
currentProjectIconBackground?: string | null;
currentProjectColor?: string | null;
onToggle: () => void;
isExpanded?: boolean;
@@ -400,15 +391,23 @@ function SessionStatusHeader({
currentSessionTitle,
currentProjectLabel,
currentProjectIcon,
currentProjectIconImageUrl,
currentProjectIconBackground,
currentProjectColor,
onToggle,
isExpanded = false,
childIndicators = []
}: SessionStatusHeaderProps) {
const [imageFailed, setImageFailed] = React.useState(false);
const ProjectIcon = currentProjectIcon ? PROJECT_ICON_MAP[currentProjectIcon] : null;
const imageUrl = !imageFailed ? currentProjectIconImageUrl : null;
const projectColorVar = currentProjectColor ? (PROJECT_COLOR_MAP[currentProjectColor] ?? null) : null;
const extraCount = childIndicators.length > 3 ? childIndicators.length - 3 : 0;
React.useEffect(() => {
setImageFailed(false);
}, [currentProjectIconImageUrl]);
return (
<button
type="button"
@@ -418,7 +417,20 @@ function SessionStatusHeader({
{!isExpanded && currentProjectLabel && (
<div className="flex flex-col items-start">
<div className="flex items-center gap-1 leading-none">
{ProjectIcon && (
{imageUrl ? (
<span
className="inline-flex h-2.5 w-2.5 items-center justify-center overflow-hidden rounded-[1px]"
style={currentProjectIconBackground ? { backgroundColor: currentProjectIconBackground } : undefined}
>
<img
src={imageUrl}
alt=""
className="h-full w-full object-contain"
draggable={false}
onError={() => setImageFailed(true)}
/>
</span>
) : ProjectIcon && (
<ProjectIcon
className="h-2.5 w-2.5"
style={projectColorVar ? { color: projectColorVar } : undefined}
@@ -534,7 +546,13 @@ function ProjectButton({
onRemoveProject,
formatProjectLabel,
}: ProjectButtonProps) {
const [imageFailed, setImageFailed] = React.useState(false);
const ProjectIcon = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
const projectIconImageUrl = !imageFailed ? getProjectIconImageUrl(project) : null;
React.useEffect(() => {
setImageFailed(false);
}, [project.id, project.iconImage?.updatedAt]);
const longPressHandlers = useLongPress(
() => {
@@ -569,7 +587,20 @@ function ProjectButton({
</div>
{/* Icon */}
{ProjectIcon && (
{projectIconImageUrl ? (
<span
className="inline-flex h-3.5 w-3.5 items-center justify-center overflow-hidden rounded-[2px]"
style={project.iconBackground ? { backgroundColor: project.iconBackground } : undefined}
>
<img
src={projectIconImageUrl}
alt=""
className="h-full w-full object-contain"
draggable={false}
onError={() => setImageFailed(true)}
/>
</span>
) : ProjectIcon && (
<ProjectIcon
className="h-3.5 w-3.5"
style={projectColorVar ? { color: projectColorVar } : undefined}
@@ -752,6 +783,8 @@ function CollapsedView({
currentSessionTitle,
currentProjectLabel,
currentProjectIcon,
currentProjectIconImageUrl,
currentProjectIconBackground,
currentProjectColor,
onToggle,
onNewSession,
@@ -764,6 +797,8 @@ function CollapsedView({
currentSessionTitle: string;
currentProjectLabel?: string;
currentProjectIcon?: string | null;
currentProjectIconImageUrl?: string | null;
currentProjectIconBackground?: string | null;
currentProjectColor?: string | null;
onToggle: () => void;
onNewSession: () => void;
@@ -789,6 +824,8 @@ function CollapsedView({
currentSessionTitle={currentSessionTitle}
currentProjectLabel={currentProjectLabel}
currentProjectIcon={currentProjectIcon}
currentProjectIconImageUrl={currentProjectIconImageUrl}
currentProjectIconBackground={currentProjectIconBackground}
currentProjectColor={currentProjectColor}
onToggle={onToggle}
childIndicators={childIndicators}
@@ -827,6 +864,8 @@ function ExpandedView({
currentSessionTitle,
currentProjectLabel,
currentProjectIcon,
currentProjectIconImageUrl,
currentProjectIconBackground,
currentProjectColor,
isExpanded,
onToggleCollapse,
@@ -854,6 +893,8 @@ function ExpandedView({
currentSessionTitle: string;
currentProjectLabel?: string;
currentProjectIcon?: string | null;
currentProjectIconImageUrl?: string | null;
currentProjectIconBackground?: string | null;
currentProjectColor?: string | null;
isExpanded: boolean;
onToggleCollapse: () => void;
@@ -936,6 +977,8 @@ function ExpandedView({
currentSessionTitle={currentSessionTitle}
currentProjectLabel={currentProjectLabel}
currentProjectIcon={currentProjectIcon}
currentProjectIconImageUrl={currentProjectIconImageUrl}
currentProjectIconBackground={currentProjectIconBackground}
currentProjectColor={currentProjectColor}
onToggle={onToggleCollapse}
isExpanded={true}
@@ -1053,6 +1096,8 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
const activeProject = getActiveProject();
const currentProjectLabel = activeProject?.label || formatDirectoryName(activeProject?.path || '', homeDirectory);
const currentProjectIcon = activeProject?.icon;
const currentProjectIconImageUrl = activeProject ? getProjectIconImageUrl(activeProject) : null;
const currentProjectIconBackground = activeProject?.iconBackground ?? null;
const currentProjectColor = activeProject?.color;
// Calculate token usage for current session
@@ -1097,8 +1142,7 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
sessionEvents.requestDirectoryDialog();
return;
}
import('@/lib/desktop')
.then(({ requestDirectoryAccess }) => requestDirectoryAccess(''))
requestDirectoryAccess('')
.then((result) => {
if (result.success && result.path) {
const added = addProject(result.path, { id: result.projectId });
@@ -1127,6 +1171,8 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
currentSessionTitle={currentSessionTitle}
currentProjectLabel={currentProjectLabel}
currentProjectIcon={currentProjectIcon}
currentProjectIconImageUrl={currentProjectIconImageUrl}
currentProjectIconBackground={currentProjectIconBackground}
currentProjectColor={currentProjectColor}
onToggle={() => setIsMobileSessionStatusBarCollapsed(false)}
onNewSession={handleCreateSession}
@@ -1146,6 +1192,8 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
currentSessionTitle={currentSessionTitle}
currentProjectLabel={currentProjectLabel}
currentProjectIcon={currentProjectIcon}
currentProjectIconImageUrl={currentProjectIconImageUrl}
currentProjectIconBackground={currentProjectIconBackground}
currentProjectColor={currentProjectColor}
isExpanded={isExpanded}
onToggleCollapse={() => {