diff --git a/apps/desktop/src/components/HistorySidebar.tsx b/apps/desktop/src/components/HistorySidebar.tsx index 32df287..794ce83 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 3b8c455..ff0bc8f 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 2f20c89..c211322 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 4a71a5f..a0e4032 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 7bf27a1..0000000 --- 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 1ece87b..c0ddf2c 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;