From ed8f6f7246050391e914fb8854f680d17303e315 Mon Sep 17 00:00:00 2001 From: arkml <6592213+arkml@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:21:24 +0530 Subject: [PATCH] Last modified (#624) * order chats by recency --- apps/x/apps/renderer/src/App.tsx | 7 +- .../src/components/chat-history-view.tsx | 11 +- .../src/components/sidebar-content.tsx | 116 ++++++------------ apps/x/packages/core/src/runs/repo.ts | 5 +- apps/x/packages/shared/src/runs.ts | 4 + 5 files changed, 55 insertions(+), 88 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index fcbc1ec7..2f4337a9 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -1042,7 +1042,7 @@ function App() { }, []) // Runs history state - type RunListItem = { id: string; title?: string; createdAt: string; agentId: string; useCase?: string } + type RunListItem = { id: string; title?: string; createdAt: string; modifiedAt: string; agentId: string; useCase?: string } const [runs, setRuns] = useState([]) // Chat tab state @@ -2716,10 +2716,12 @@ function App() { const inferredTitle = inferRunTitleFromMessage(titleSource) setRuns((prev) => { const withoutCurrent = prev.filter((run) => run.id !== currentRunId) + const createdAt = newRunCreatedAt ?? new Date().toISOString() return [{ id: currentRunId!, title: inferredTitle, - createdAt: newRunCreatedAt ?? new Date().toISOString(), + createdAt, + modifiedAt: createdAt, agentId, }, ...withoutCurrent] }) @@ -5496,6 +5498,7 @@ function App() { onOpenAgent={(slug) => { setBgTaskInitialSlug(slug); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }} recentRuns={runs} onOpenRun={(rid) => void navigateToView({ type: 'chat', runId: rid })} + onOpenChatHistory={() => void navigateToView({ type: 'chat-history' })} onOpenEmail={(threadId) => openEmailView(threadId)} onOpenHome={() => void navigateToView({ type: 'home' })} onNewChat={handleNewChatTab} diff --git a/apps/x/apps/renderer/src/components/chat-history-view.tsx b/apps/x/apps/renderer/src/components/chat-history-view.tsx index 44ab9f4c..831a8aa1 100644 --- a/apps/x/apps/renderer/src/components/chat-history-view.tsx +++ b/apps/x/apps/renderer/src/components/chat-history-view.tsx @@ -23,6 +23,7 @@ type Run = { id: string title?: string createdAt: string + modifiedAt: string agentId: string } @@ -51,8 +52,8 @@ export function ChatHistoryView({ const sortedRuns = useMemo(() => { return [...runs].sort((a, b) => { - const at = new Date(a.createdAt).getTime() - const bt = new Date(b.createdAt).getTime() + const at = new Date(a.modifiedAt).getTime() + const bt = new Date(b.modifiedAt).getTime() return (Number.isNaN(bt) ? 0 : bt) - (Number.isNaN(at) ? 0 : at) }) }, [runs]) @@ -92,7 +93,7 @@ export function ChatHistoryView({
Title
-
Created
+
Last modified
{sortedRuns.length === 0 ? ( @@ -122,8 +123,8 @@ export function ChatHistoryView({ {run.title || '(Untitled chat)'}
-
- {formatRelativeTime(run.createdAt)} +
+ {formatRelativeTime(run.modifiedAt)}
diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index 62185d63..8de6402a 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -3,6 +3,7 @@ import * as React from "react" import { useCallback, useEffect, useRef, useState } from "react" import { + ArrowUpRight, Bot, ChevronRight, Code2, @@ -90,13 +91,6 @@ type KnowledgeActions = { onOpenInNewTab?: (path: string) => void } -function displayNoteName(node: TreeNode): string { - if (node.kind === 'file' && node.name.toLowerCase().endsWith('.md')) { - return node.name.slice(0, -3) - } - return node.name -} - function formatBillingPlanName(plan: string | null | undefined) { if (!plan) return 'No plan' return `${plan.charAt(0).toUpperCase()}${plan.slice(1)} plan` @@ -172,8 +166,9 @@ type SidebarContentPanelProps = { onOpenCode?: () => void onOpenBgTasks?: () => void onOpenAgent?: (slug: string) => void - recentRuns?: { id: string; title?: string; createdAt: string }[] + recentRuns?: { id: string; title?: string; createdAt: string; modifiedAt?: string }[] onOpenRun?: (runId: string) => void + onOpenChatHistory?: () => void onOpenEmail?: (threadId?: string) => void onOpenHome?: () => void onNewChat?: () => void @@ -414,15 +409,14 @@ function SyncStatusBar() { export function SidebarContentPanel({ tree, - onSelectFile, knowledgeActions, bgTaskSummaries = [], onOpenMeetings, onOpenCode, onOpenBgTasks, - onOpenAgent, recentRuns = [], onOpenRun, + onOpenChatHistory, onOpenEmail, onOpenHome, onNewChat, @@ -448,7 +442,7 @@ export function SidebarContentPanel({ const [unreadEmailCount, setUnreadEmailCount] = useState(0) const [emailThreads, setEmailThreads] = useState([]) const [meetings, setMeetings] = useState([]) - const [quickAccessExpanded, setQuickAccessExpanded] = useState(true) + const [chatsExpanded, setChatsExpanded] = useState(true) // The Code section only makes sense with a coding agent available — same // flag the chat composer's code chip uses (auto-on when Claude Code or // Codex is installed + signed in; explicit toggle in settings wins). @@ -542,59 +536,16 @@ export function SidebarContentPanel({ .slice(0, 10) }, [tree]) - // Recents: most recently touched notes / agents / chats, interleaved by - // recency. Capped per type (4 notes, 4 agents, 4 chats) and 12 overall. - type QuickAccessItem = { - key: string - label: string - recency: number - type: 'note' | 'agent' | 'chat' - onClick: () => void - } - const quickAccessItems = React.useMemo(() => { - const items: QuickAccessItem[] = [] - - for (const note of recentNotes.slice(0, 4)) { - items.push({ - key: `note:${note.path}`, - label: displayNoteName(note), - recency: note.stat?.mtimeMs ?? 0, - type: 'note', - onClick: () => onSelectFile(note.path, 'file'), - }) - } - - const agentRecency = (t: TaskSummary) => { - const ts = t.lastRunAt ?? t.lastAttemptAt ?? t.createdAt - const ms = ts ? new Date(ts).getTime() : 0 + // Chats: the 5 most recently modified chats, newest first. + const recentChats = React.useMemo(() => { + const chatRecency = (r: { createdAt: string; modifiedAt?: string }) => { + const ms = new Date(r.modifiedAt ?? r.createdAt).getTime() return Number.isFinite(ms) ? ms : 0 } - for (const t of [...bgTaskSummaries].sort((a, b) => agentRecency(b) - agentRecency(a)).slice(0, 4)) { - items.push({ - key: `agent:${t.slug}`, - label: t.name, - recency: agentRecency(t), - type: 'agent', - onClick: () => onOpenAgent?.(t.slug), - }) - } - - const chatRecency = (r: { createdAt: string }) => { - const ms = new Date(r.createdAt).getTime() - return Number.isFinite(ms) ? ms : 0 - } - for (const r of [...recentRuns].sort((a, b) => chatRecency(b) - chatRecency(a)).slice(0, 4)) { - items.push({ - key: `chat:${r.id}`, - label: r.title || '(Untitled chat)', - recency: chatRecency(r), - type: 'chat', - onClick: () => onOpenRun?.(r.id), - }) - } - - return items.sort((a, b) => b.recency - a.recency).slice(0, 12) - }, [recentNotes, bgTaskSummaries, recentRuns, onSelectFile, onOpenAgent, onOpenRun]) + return [...recentRuns] + .sort((a, b) => chatRecency(b) - chatRecency(a)) + .slice(0, 10) + }, [recentRuns]) // Workspace count for the Workspaces sublabel — top-level dir children of // knowledge/Workspace (matches WorkspaceView's root listing). @@ -921,38 +872,43 @@ export function SidebarContentPanel({
- {/* Recents */} + {/* Chats */} - {quickAccessExpanded && ( - quickAccessItems.length === 0 ? ( + {chatsExpanded && ( + recentChats.length === 0 ? (
- Recent notes and agents show up here. + Your recent chats show up here.
) : ( - {quickAccessItems.map((item) => ( - - - {item.type === 'agent' ? ( - - ) : item.type === 'chat' ? ( - - ) : ( - - )} - {item.label} + {recentChats.map((chat) => ( + + onOpenRun?.(chat.id)}> + + {chat.title || '(Untitled chat)'} ))} + {onOpenChatHistory && ( + + onOpenChatHistory()} + className="text-muted-foreground" + > + + View all + + + )} ) )} diff --git a/apps/x/packages/core/src/runs/repo.ts b/apps/x/packages/core/src/runs/repo.ts index 19dccfda..f08cac7c 100644 --- a/apps/x/packages/core/src/runs/repo.ts +++ b/apps/x/packages/core/src/runs/repo.ts @@ -298,14 +298,17 @@ export class FSRunsRepo implements IRunsRepo { for (const name of selected) { const runId = name.slice(0, -'.jsonl'.length); - const metadata = await this.readRunMetadata(path.join(runsDir, name)); + const filePath = path.join(runsDir, name); + const metadata = await this.readRunMetadata(filePath); if (!metadata) { continue; } + const stat = await fsp.stat(filePath); runs.push({ id: runId, title: metadata.title, createdAt: metadata.start.ts!, + modifiedAt: stat.mtime.toISOString(), agentId: metadata.start.agentName, ...(metadata.start.useCase ? { useCase: metadata.start.useCase } : {}), }); diff --git a/apps/x/packages/shared/src/runs.ts b/apps/x/packages/shared/src/runs.ts index 496db9ba..571bf95a 100644 --- a/apps/x/packages/shared/src/runs.ts +++ b/apps/x/packages/shared/src/runs.ts @@ -212,6 +212,10 @@ export const ListRunsResponse = z.object({ createdAt: true, agentId: true, useCase: true, + }).extend({ + // Last-modified time of the run's log file (mtime), used to order the + // chat history by recent activity rather than creation time. + modifiedAt: z.iso.datetime(), })), nextCursor: z.string().optional(), });