From 203f467d7495a6e28ba330a64da0600de2327184 Mon Sep 17 00:00:00 2001 From: 1bcMax <195689928+1bcMax@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:36:48 -0500 Subject: [PATCH] =?UTF-8?q?fix(desktop):=20sidebar=20polish=20=E2=80=94=20?= =?UTF-8?q?pin=20ordering,=20i18n,=20dead=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four leftovers from the #147 sidebar reorganization that #148 did not cover. Pinning no longer reorders Recent. `updatedAt` doubles as the cloud-sync revision — cloud-sync.ts only pushes a conversation when that value changes — so togglePinned has to bump it, which meant pinning and then unpinning a months old chat threw it to the top of Recent with no new messages. Adds `activityAt`, set by the three real content mutations (setMessages, renameChat, deleteMedia) and left alone by togglePinned, and sorts the sidebar and search on it. Conversations saved before the field fall back to `updatedAt`. Sidebar strings are translated. "Agents", "New project", "Pinned", "Projects", "Recent", "Connecting…", "No projects", the Team Mode tooltips, and the row pin/delete labels were hardcoded English next to t.newChat and t.gallery in the same file, so 中文 and Español users got a half-translated sidebar. Adds 13 keys across all three locales and drops `history`, which the reorganization orphaned. Empty sections no longer render a bare header. Pinned has no empty-state text, so with nothing pinned it showed a "PINNED" label over nothing; it is now hidden when empty. Recent's empty note was gated on there being no conversations at all, so pinning every chat left "RECENT" blank. Deletes lib/team-workspace-events.ts. #147 replaced the window CustomEvent bus with props, leaving all four functions and both event constants uncalled; the one surviving type moves next to CloudWorkspace. Also drops the CSS the same commit orphaned: .try-space-switch (7 rules), .try-history-group(-label), and the .try-history wrapper. Verified: typecheck, eslint, vite build, and the desktop suites all pass. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EdbfY8FiReoKH32UKeKfzN --- .../desktop/src/components/HistorySidebar.tsx | 45 +++++++-------- apps/desktop/src/hooks/use-chat-history.ts | 19 ++++++- apps/desktop/src/hooks/use-cloud-workspace.ts | 9 +++ apps/desktop/src/lib/i18n.tsx | 56 +++++++++++++++++-- apps/desktop/src/lib/team-workspace-events.ts | 36 ------------ apps/desktop/src/styles/globals.css | 48 ---------------- 6 files changed, 101 insertions(+), 112 deletions(-) delete mode 100644 apps/desktop/src/lib/team-workspace-events.ts diff --git a/apps/desktop/src/components/HistorySidebar.tsx b/apps/desktop/src/components/HistorySidebar.tsx index 32df2879..794ce836 100644 --- a/apps/desktop/src/components/HistorySidebar.tsx +++ b/apps/desktop/src/components/HistorySidebar.tsx @@ -3,14 +3,14 @@ import { Plus, MessageSquare, Trash2, Phone, Blocks, Images, Wallet, Sparkles, Search, Grid2x2, ChevronRight, Terminal, Server, Bot, Folder, FolderPlus, Pin, PinOff, } from "lucide-react"; -import type { ChatSpace, Conversation } from "../hooks/use-chat-history"; +import { conversationActivityAt, type ChatSpace, type Conversation } from "../hooks/use-chat-history"; import type { WalletInfo } from "../lib/wire"; import type { AgentConnectionState } from "../lib/ws"; import { useTryLang } from "../lib/i18n"; import { MoreMenu } from "./MoreMenu"; import { WalletPill } from "./WalletPill"; import franklinAvatar from "../assets/franklin-avatar.png"; -import type { TeamWorkspaceNavItem } from "../lib/team-workspace-events"; +import type { TeamWorkspaceNavItem } from "../hooks/use-cloud-workspace"; import { useSidebarPreferences } from "../hooks/use-sidebar-preferences"; export type TryView = "chat" | "agents" | "phone" | "tools" | "gallery" | "wallet" | "skills" | "cli" | "mcp"; @@ -58,7 +58,7 @@ export function HistorySidebar({ conversations, activeId, onNewChat, onNewProjec setMoreOpen(true); }; - const sorted = [...conversations].sort((a, b) => b.updatedAt - a.updatedAt); + const sorted = [...conversations].sort((a, b) => conversationActivityAt(b) - conversationActivityAt(a)); const pinned = sorted.filter((conversation) => conversation.pinnedAt).sort((a, b) => (b.pinnedAt ?? 0) - (a.pinnedAt ?? 0)); const recent = sorted.filter((conversation) => !conversation.pinnedAt); @@ -100,22 +100,22 @@ export function HistorySidebar({ conversations, activeId, onNewChat, onNewProjec
{visibleItems.includes("agents") && } - {!teamModeEnabled && } + {!teamModeEnabled && } - + {pinned.length > 0 && {pinned.map((conversation) => ( ))} - + } - + {teamLoading ? ( -

Connecting…

+

{t.connectingProjects}

) : teamWorkspaces.length === 0 ? ( -

No projects

+

{t.noProjects}

) : teamWorkspaces.map((workspace) => ( )) )} diff --git a/apps/desktop/src/hooks/use-chat-history.ts b/apps/desktop/src/hooks/use-chat-history.ts index 3b8c455d..ff0bc8fb 100644 --- a/apps/desktop/src/hooks/use-chat-history.ts +++ b/apps/desktop/src/hooks/use-chat-history.ts @@ -26,6 +26,18 @@ export interface Conversation { space?: ChatSpace; /** Unix timestamp used to order conversations pinned in the sidebar. */ pinnedAt?: number; + /** + * Last time the conversation itself changed (messages, title, media). + * Distinct from `updatedAt`, which doubles as the cloud-sync revision and so + * has to move for metadata-only edits like pinning. Sidebar ordering reads + * this; records written before it existed fall back to `updatedAt`. + */ + activityAt?: number; +} + +/** Sidebar ordering key. Falls back for conversations saved before `activityAt`. */ +export function conversationActivityAt(conversation: Conversation): number { + return conversation.activityAt ?? conversation.updatedAt; } type Setter = ChatMessage[] | ((prev: ChatMessage[]) => ChatMessage[]); @@ -151,7 +163,8 @@ export function useChatHistory(address: string | null, space: ChatSpace = "perso const renameChat = useCallback((id: string, title: string) => { const clean = title.trim().slice(0, 80) || "New chat"; - setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, title: clean, updatedAt: Date.now() } : c))); + const now = Date.now(); + setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, title: clean, updatedAt: now, activityAt: now } : c))); }, []); const deleteChat = useCallback( @@ -174,7 +187,7 @@ export function useChatHistory(address: string | null, space: ChatSpace = "perso if (activeBySpaceRef.current[space] === convId) setActiveId(null); return prev.filter((c) => c.id !== convId); } - const updated = { ...cur, messages: msgs, updatedAt: Date.now() }; + const updated = { ...cur, messages: msgs, updatedAt: Date.now(), activityAt: Date.now() }; return prev.map((c) => (c.id === convId ? updated : c)); }); }, @@ -218,6 +231,7 @@ export function useChatHistory(address: string | null, space: ChatSpace = "perso title: titleFrom(msgs), createdAt: now, updatedAt: now, + activityAt: now, messages: msgs, space: pendingSpaceRef.current[id] ?? space, }; @@ -229,6 +243,7 @@ export function useChatHistory(address: string | null, space: ChatSpace = "perso messages: msgs, title: cur.title && cur.title !== "New chat" ? cur.title : titleFrom(msgs), updatedAt: now, + activityAt: now, }; arr = prev.map((c) => (c.id === id ? updated : c)); } diff --git a/apps/desktop/src/hooks/use-cloud-workspace.ts b/apps/desktop/src/hooks/use-cloud-workspace.ts index 2f20c89c..c2113222 100644 --- a/apps/desktop/src/hooks/use-cloud-workspace.ts +++ b/apps/desktop/src/hooks/use-cloud-workspace.ts @@ -6,6 +6,15 @@ export interface CloudWorkspace { id: string; name: string; createdAt: string; updatedAt?: string; version: number; runtime: string; role: CloudMember["role"]; members: CloudMember[]; } +/** Flattened workspace shape the sidebar renders. */ +export interface TeamWorkspaceNavItem { + id: string; + name: string; + role: CloudMember["role"]; + memberCount: number; + version: number; +} + export interface CloudMessage { id: string; role: "user" | "assistant"; authorId: string; authorName: string; content: string; createdAt: string; } diff --git a/apps/desktop/src/lib/i18n.tsx b/apps/desktop/src/lib/i18n.tsx index 4a71a5f2..a0e40323 100644 --- a/apps/desktop/src/lib/i18n.tsx +++ b/apps/desktop/src/lib/i18n.tsx @@ -13,7 +13,19 @@ export const TRY_LANGS: { id: TryLang; label: string }[] = [ export interface TryDict { newChat: string; noConversations: string; - history: string; + pinned: string; + recent: string; + projects: string; + agents: string; + newProject: string; + noProjects: string; + connectingProjects: string; + createOrJoinProject: string; + enableTeamMode: string; + teamModeOff: string; + pinConversation: string; + unpinConversation: string; + deleteConversation: string; connectWallet: string; connecting: string; installWallet: string; @@ -151,7 +163,19 @@ export interface TryDict { const en: TryDict = { newChat: "New chat", noConversations: "No conversations yet.", - history: "History", + pinned: "Pinned", + recent: "Recent", + projects: "Projects", + agents: "Agents", + newProject: "New project", + noProjects: "No projects", + connectingProjects: "Connecting…", + createOrJoinProject: "Create or join a project", + enableTeamMode: "Enable Team Mode in Agents", + teamModeOff: "Team Mode is off · Manage modules", + pinConversation: "Pin conversation", + unpinConversation: "Unpin conversation", + deleteConversation: "Delete conversation", connectWallet: "Connect wallet", connecting: "Connecting…", installWallet: "Install a wallet", @@ -290,7 +314,19 @@ const en: TryDict = { const zh: TryDict = { newChat: "新对话", noConversations: "还没有对话。", - history: "历史记录", + pinned: "已置顶", + recent: "最近", + projects: "项目", + agents: "智能体", + newProject: "新建项目", + noProjects: "暂无项目", + connectingProjects: "连接中…", + createOrJoinProject: "创建或加入项目", + enableTeamMode: "在「智能体」中开启团队模式", + teamModeOff: "团队模式已关闭 · 管理模块", + pinConversation: "置顶对话", + unpinConversation: "取消置顶", + deleteConversation: "删除对话", connectWallet: "连接钱包", connecting: "连接中…", installWallet: "安装钱包", @@ -429,7 +465,19 @@ const zh: TryDict = { const es: TryDict = { newChat: "Nuevo chat", noConversations: "Aún no hay conversaciones.", - history: "Historial", + pinned: "Fijados", + recent: "Recientes", + projects: "Proyectos", + agents: "Agentes", + newProject: "Nuevo proyecto", + noProjects: "Sin proyectos", + connectingProjects: "Conectando…", + createOrJoinProject: "Crear o unirse a un proyecto", + enableTeamMode: "Activa el Modo Equipo en Agentes", + teamModeOff: "Modo Equipo desactivado · Gestionar módulos", + pinConversation: "Fijar conversación", + unpinConversation: "Dejar de fijar", + deleteConversation: "Eliminar conversación", connectWallet: "Conectar billetera", connecting: "Conectando…", installWallet: "Instalar billetera", diff --git a/apps/desktop/src/lib/team-workspace-events.ts b/apps/desktop/src/lib/team-workspace-events.ts deleted file mode 100644 index 7bf27a13..00000000 --- a/apps/desktop/src/lib/team-workspace-events.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface TeamWorkspaceNavItem { - id: string; - name: string; - role: "owner" | "admin" | "member" | "viewer"; - memberCount: number; - version: number; -} - -export interface TeamWorkspaceNavState { - items: TeamWorkspaceNavItem[]; - activeId: string | null; - loading: boolean; -} - -const NAV_EVENT = "franklin:team-workspaces"; -const SELECT_EVENT = "franklin:team-workspace-select"; - -export function publishTeamWorkspaceNav(state: TeamWorkspaceNavState) { - window.dispatchEvent(new CustomEvent(NAV_EVENT, { detail: state })); -} - -export function subscribeTeamWorkspaceNav(listener: (state: TeamWorkspaceNavState) => void) { - const handler = (event: Event) => listener((event as CustomEvent).detail); - window.addEventListener(NAV_EVENT, handler); - return () => window.removeEventListener(NAV_EVENT, handler); -} - -export function requestTeamWorkspace(id: string | null) { - window.dispatchEvent(new CustomEvent(SELECT_EVENT, { detail: id })); -} - -export function subscribeTeamWorkspaceRequest(listener: (id: string | null) => void) { - const handler = (event: Event) => listener((event as CustomEvent).detail); - window.addEventListener(SELECT_EVENT, handler); - return () => window.removeEventListener(SELECT_EVENT, handler); -} diff --git a/apps/desktop/src/styles/globals.css b/apps/desktop/src/styles/globals.css index 1ece87bb..c0ddf2ca 100644 --- a/apps/desktop/src/styles/globals.css +++ b/apps/desktop/src/styles/globals.css @@ -4630,39 +4630,6 @@ h2.dark-h, } /* Personal / Team workspace demo switcher. */ -.try-space-switch { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 4px; - padding: 3px; - margin: 2px 2px 8px; - background: color-mix(in oklch, var(--fg) 5%, transparent); - border: 1px solid var(--border); - border-radius: 11px; -} -.try-space-switch button { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - min-width: 0; - padding: 7px 8px; - border: 0; - border-radius: 8px; - background: transparent; - color: var(--fg-subtle); - font-size: 12.5px; - font-weight: 600; - cursor: pointer; - transition: background 0.14s, color 0.14s, box-shadow 0.14s; -} -.try-space-switch button:hover { color: var(--fg); } -.try-space-switch button.is-active { - background: var(--bg); - color: var(--fg); - box-shadow: 0 1px 4px rgba(31, 28, 22, 0.09); -} -.try-space-switch button.is-active svg { color: var(--gold-dim); } .try-team-beta { padding: 1px 4px; border: 1px solid color-mix(in oklch, var(--gold-dim) 35%, transparent); @@ -5181,19 +5148,6 @@ h2.dark-h, .try-search-result.is-new svg { color: var(--gold-dim); } /* History date groups */ -.try-history-group { margin-bottom: 6px; } -.try-history-group-label { - position: sticky; - top: 0; - z-index: 1; - background: var(--bg-alt); - font-family: var(--font-mono); - font-size: 10px; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--fg-subtle); - padding: 6px 6px 6px; -} /* Wallet panel */ .try-wallet-panel { @@ -5799,7 +5753,6 @@ h2.dark-h, .try-sidebar-secondary { display: grid; gap: 1px; margin-top: 7px; padding-top: 7px; border-top: 1px solid var(--border); } .try-project-create { color: var(--fg); } .try-project-create svg { color: var(--gold-dim); } -.try-history { display: flex; flex-direction: column; gap: 1px; } .try-history-empty { font-size: 13px; color: var(--fg-subtle); @@ -6729,7 +6682,6 @@ html.is-mac .try-brand { text-transform: uppercase; color: var(--fg-subtle); } -.try-space-switch button:disabled { opacity: .38; cursor: not-allowed; } .try-team-disabled-note { width: 100%; margin: -2px 0 8px;