From b91a9b0185b458cb760c68693aba6bd6e6c118d3 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Mon, 12 Jan 2026 16:29:09 -0800 Subject: [PATCH 1/7] reorder workspace threads by latest interaction --- .../ThreadContainer/ThreadItem/index.jsx | 3 +- .../ThreadContainer/index.jsx | 33 +++++++++++++++++++ .../WorkspaceChat/ChatContainer/index.jsx | 3 +- frontend/src/index.css | 20 +++++++++++ frontend/src/utils/chat/index.js | 14 ++++++-- server/endpoints/workspaceThreads.js | 12 ++++--- server/models/workspaceThread.js | 13 ++++++++ server/utils/chats/stream.js | 3 ++ 8 files changed, 93 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx index be9b6762f17..14665cd5ebb 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx @@ -22,6 +22,7 @@ export default function ThreadItem({ toggleMarkForDeletion, hasNext, ctrlPressed = false, + wasBumped = false, }) { const { slug, threadSlug = null } = useParams(); const optionsContainer = useRef(null); @@ -32,7 +33,7 @@ export default function ThreadItem({ return (
{/* Curved line Element and leader if required */} diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx index f9c0ea4edb7..3eb8ec7b458 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx @@ -6,12 +6,14 @@ import { useEffect, useState } from "react"; import ThreadItem from "./ThreadItem"; import { useParams } from "react-router-dom"; export const THREAD_RENAME_EVENT = "renameThread"; +export const THREAD_ACTIVITY_EVENT = "threadActivity"; export default function ThreadContainer({ workspace }) { const { threadSlug = null } = useParams(); const [threads, setThreads] = useState([]); const [loading, setLoading] = useState(true); const [ctrlPressed, setCtrlPressed] = useState(false); + const [bumpedThreadSlug, setBumpedThreadSlug] = useState(null); useEffect(() => { const chatHandler = (event) => { @@ -33,6 +35,36 @@ export default function ThreadContainer({ workspace }) { }; }, []); + useEffect(() => { + const activityHandler = (event) => { + const { threadSlug: activeSlug } = event.detail; + if (!activeSlug) return; + + setThreads((prevThreads) => { + const idx = prevThreads.findIndex((t) => t.slug === activeSlug); + if (idx <= 0) return prevThreads; + + // Move thread to top + const thread = prevThreads[idx]; + const reordered = [ + thread, + ...prevThreads.slice(0, idx), + ...prevThreads.slice(idx + 1), + ]; + setBumpedThreadSlug(activeSlug); + + // Wait for animation before resetting + setTimeout(() => setBumpedThreadSlug(null), 800); + return reordered; + }); + }; + + window.addEventListener(THREAD_ACTIVITY_EVENT, activityHandler); + return () => { + window.removeEventListener(THREAD_ACTIVITY_EVENT, activityHandler); + }; + }, []); + useEffect(() => { async function fetchThreads() { if (!workspace.slug) return; @@ -144,6 +176,7 @@ export default function ThreadContainer({ workspace }) { onRemove={removeThread} thread={thread} hasNext={i !== threads.length - 1} + wasBumped={thread.slug === bumpedThreadSlug} /> ))} Date: Tue, 13 Jan 2026 14:55:24 -0800 Subject: [PATCH 2/7] reorder threads on slash command usage --- frontend/src/utils/chat/index.js | 22 +++++++++++++++++----- server/utils/chats/commands/reset.js | 3 +++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/frontend/src/utils/chat/index.js b/frontend/src/utils/chat/index.js index 59074b54fc6..cdc1d23dc0e 100644 --- a/frontend/src/utils/chat/index.js +++ b/frontend/src/utils/chat/index.js @@ -113,11 +113,14 @@ export default function handleChat( emitAssistantMessageCompleteEvent(chatId); setLoadingResponse(false); - window.dispatchEvent( - new CustomEvent(THREAD_ACTIVITY_EVENT, { - detail: { threadSlug }, - }) - ); + // Move thread to top + if (threadSlug) { + window.dispatchEvent( + new CustomEvent(THREAD_ACTIVITY_EVENT, { + detail: { threadSlug }, + }) + ); + } } else { updatedHistory = { ...existingHistory, @@ -171,6 +174,15 @@ export default function handleChat( if (action === "reset_chat") { // Chat was reset, keep reset message and clear everything else. setChatHistory([_chatHistory.pop()]); + + // Move thread to top + if (threadSlug) { + window.dispatchEvent( + new CustomEvent(THREAD_ACTIVITY_EVENT, { + detail: { threadSlug }, + }) + ); + } } // If thread was updated automatically based on chat prompt diff --git a/server/utils/chats/commands/reset.js b/server/utils/chats/commands/reset.js index f2bd4562c8e..c0914990673 100644 --- a/server/utils/chats/commands/reset.js +++ b/server/utils/chats/commands/reset.js @@ -1,4 +1,5 @@ const { WorkspaceChats } = require("../../../models/workspaceChats"); +const { WorkspaceThread } = require("../../../models/workspaceThread"); async function resetMemory( workspace, @@ -16,6 +17,8 @@ async function resetMemory( ) : await WorkspaceChats.markHistoryInvalid(workspace.id, user); + if (thread?.id) await WorkspaceThread.touchActivity(thread.id); + return { uuid: msgUUID, type: "textResponse", From e0d62f90892e39bf255683c16bc2df857c5e7e49 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Tue, 13 Jan 2026 15:51:44 -0800 Subject: [PATCH 3/7] reduce usage of touchAcivity by modifying WorkspaceChats.new to update timestamp --- server/models/workspaceChats.js | 9 +++++++++ server/models/workspaceThread.js | 2 +- server/utils/chats/stream.js | 3 --- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/server/models/workspaceChats.js b/server/models/workspaceChats.js index e48807be71d..41fd23e2689 100644 --- a/server/models/workspaceChats.js +++ b/server/models/workspaceChats.js @@ -23,6 +23,15 @@ const WorkspaceChats = { include, }, }); + + // Update thread timestamp for ordering + if (threadId) { + await prisma.workspace_threads.update({ + where: { id: threadId }, + data: { lastUpdatedAt: new Date() }, + }); + } + return { chat, message: null }; } catch (error) { console.error(error.message); diff --git a/server/models/workspaceThread.js b/server/models/workspaceThread.js index ab641feb8a7..07a5ed0d16a 100644 --- a/server/models/workspaceThread.js +++ b/server/models/workspaceThread.js @@ -129,7 +129,7 @@ const WorkspaceThread = { data: { lastUpdatedAt: new Date() }, }); } catch (error) { - console.error("Failed to touch thread activity:", error.message); + console.error(error.message); } }, diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index d5824ae78b2..acb1e4a5c8a 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -1,7 +1,6 @@ const { v4: uuidv4 } = require("uuid"); const { DocumentManager } = require("../DocumentManager"); const { WorkspaceChats } = require("../../models/workspaceChats"); -const { WorkspaceThread } = require("../../models/workspaceThread"); const { WorkspaceParsedFiles } = require("../../models/workspaceParsedFiles"); const { getVectorDbClass, getLLMProvider } = require("../helpers"); const { writeResponseChunk } = require("../helpers/chat/responses"); @@ -290,8 +289,6 @@ async function streamChatWithWorkspace( user, }); - if (thread?.id) await WorkspaceThread.touchActivity(thread.id); - writeResponseChunk(response, { uuid, type: "finalizeResponseStream", From f02d52a5e645ddea9e03dc7bb81cbc8907c67751 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Tue, 13 Jan 2026 16:16:50 -0800 Subject: [PATCH 4/7] reorder threads on agent chat --- .../components/WorkspaceChat/ChatContainer/index.jsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx index 413ff7ddfd6..7e29c19ec4c 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx @@ -22,6 +22,7 @@ import SpeechRecognition, { } from "react-speech-recognition"; import { ChatTooltips } from "./ChatTooltips"; import { MetricsProvider } from "./ChatHistory/HistoricalMessage/Actions/RenderMetrics"; +import { THREAD_ACTIVITY_EVENT } from "@/components/Sidebar/ActiveWorkspaces/ThreadContainer"; export default function ChatContainer({ workspace, knownHistory = [] }) { const { threadSlug = null } = useParams(); @@ -276,6 +277,16 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { }); setWebsocket(socket); window.dispatchEvent(new CustomEvent(AGENT_SESSION_START)); + + // Move thread to top + if (threadSlug) { + window.dispatchEvent( + new CustomEvent(THREAD_ACTIVITY_EVENT, { + detail: { threadSlug }, + }) + ); + } + window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT)); } catch (e) { setChatHistory((prev) => [ From da1151631758cea48831d8ef86c6261211123358 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Tue, 13 Jan 2026 16:24:46 -0800 Subject: [PATCH 5/7] handle query mode no context --- frontend/src/utils/chat/index.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/utils/chat/index.js b/frontend/src/utils/chat/index.js index cdc1d23dc0e..528958ae707 100644 --- a/frontend/src/utils/chat/index.js +++ b/frontend/src/utils/chat/index.js @@ -87,6 +87,15 @@ export default function handleChat( metrics, }); emitAssistantMessageCompleteEvent(chatId); + + // Move thread to top + if (threadSlug) { + window.dispatchEvent( + new CustomEvent(THREAD_ACTIVITY_EVENT, { + detail: { threadSlug }, + }) + ); + } } else if ( type === "textResponseChunk" || type === "finalizeResponseStream" From 5f467319eb9265ba29e0138e72321cdbedc0e27b Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Tue, 13 Jan 2026 16:33:14 -0800 Subject: [PATCH 6/7] handle timeout race condition --- .../ThreadContainer/index.jsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx index 3eb8ec7b458..d12c287c726 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx @@ -2,7 +2,7 @@ import Workspace from "@/models/workspace"; import paths from "@/utils/paths"; import showToast from "@/utils/toast"; import { Plus, CircleNotch, Trash } from "@phosphor-icons/react"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import ThreadItem from "./ThreadItem"; import { useParams } from "react-router-dom"; export const THREAD_RENAME_EVENT = "renameThread"; @@ -14,6 +14,7 @@ export default function ThreadContainer({ workspace }) { const [loading, setLoading] = useState(true); const [ctrlPressed, setCtrlPressed] = useState(false); const [bumpedThreadSlug, setBumpedThreadSlug] = useState(null); + const timeoutRef = useRef(null); useEffect(() => { const chatHandler = (event) => { @@ -51,10 +52,19 @@ export default function ThreadContainer({ workspace }) { ...prevThreads.slice(0, idx), ...prevThreads.slice(idx + 1), ]; + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + setBumpedThreadSlug(activeSlug); // Wait for animation before resetting - setTimeout(() => setBumpedThreadSlug(null), 800); + timeoutRef.current = setTimeout(() => { + setBumpedThreadSlug(null); + timeoutRef.current = null; + }, 800); + return reordered; }); }; @@ -62,6 +72,11 @@ export default function ThreadContainer({ workspace }) { window.addEventListener(THREAD_ACTIVITY_EVENT, activityHandler); return () => { window.removeEventListener(THREAD_ACTIVITY_EVENT, activityHandler); + + // Cleanup timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } }; }, []); From 76808b2362a873a246195d1357ee364e90a06e6b Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Wed, 14 Jan 2026 13:20:51 -0800 Subject: [PATCH 7/7] create dispatchThreadActivityEvent to reduce code duplication --- .../ThreadContainer/index.jsx | 9 +++++++ .../WorkspaceChat/ChatContainer/index.jsx | 10 ++----- frontend/src/utils/chat/index.js | 26 +++---------------- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx index d12c287c726..25a4bcd8470 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx @@ -279,3 +279,12 @@ function DeleteAllThreadButton({ ctrlPressed, threads, onDelete }) { ); } + +export function dispatchThreadActivityEvent(threadSlug) { + if (!threadSlug) return; + window.dispatchEvent( + new CustomEvent(THREAD_ACTIVITY_EVENT, { + detail: { threadSlug }, + }) + ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx index 7e29c19ec4c..8e31153d1e1 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx @@ -16,13 +16,13 @@ import handleSocketResponse, { AGENT_SESSION_END, AGENT_SESSION_START, } from "@/utils/chat/agent"; +import { dispatchThreadActivityEvent } from "@/components/Sidebar/ActiveWorkspaces/ThreadContainer"; import DnDFileUploaderWrapper from "./DnDWrapper"; import SpeechRecognition, { useSpeechRecognition, } from "react-speech-recognition"; import { ChatTooltips } from "./ChatTooltips"; import { MetricsProvider } from "./ChatHistory/HistoricalMessage/Actions/RenderMetrics"; -import { THREAD_ACTIVITY_EVENT } from "@/components/Sidebar/ActiveWorkspaces/ThreadContainer"; export default function ChatContainer({ workspace, knownHistory = [] }) { const { threadSlug = null } = useParams(); @@ -279,13 +279,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { window.dispatchEvent(new CustomEvent(AGENT_SESSION_START)); // Move thread to top - if (threadSlug) { - window.dispatchEvent( - new CustomEvent(THREAD_ACTIVITY_EVENT, { - detail: { threadSlug }, - }) - ); - } + dispatchThreadActivityEvent(threadSlug); window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT)); } catch (e) { diff --git a/frontend/src/utils/chat/index.js b/frontend/src/utils/chat/index.js index 528958ae707..e5942747fea 100644 --- a/frontend/src/utils/chat/index.js +++ b/frontend/src/utils/chat/index.js @@ -1,6 +1,6 @@ import { THREAD_RENAME_EVENT, - THREAD_ACTIVITY_EVENT, + dispatchThreadActivityEvent, } from "@/components/Sidebar/ActiveWorkspaces/ThreadContainer"; import { emitAssistantMessageCompleteEvent } from "@/components/contexts/TTSProvider"; export const ABORT_STREAM_EVENT = "abort-chat-stream"; @@ -89,13 +89,7 @@ export default function handleChat( emitAssistantMessageCompleteEvent(chatId); // Move thread to top - if (threadSlug) { - window.dispatchEvent( - new CustomEvent(THREAD_ACTIVITY_EVENT, { - detail: { threadSlug }, - }) - ); - } + dispatchThreadActivityEvent(threadSlug); } else if ( type === "textResponseChunk" || type === "finalizeResponseStream" @@ -123,13 +117,7 @@ export default function handleChat( setLoadingResponse(false); // Move thread to top - if (threadSlug) { - window.dispatchEvent( - new CustomEvent(THREAD_ACTIVITY_EVENT, { - detail: { threadSlug }, - }) - ); - } + dispatchThreadActivityEvent(threadSlug); } else { updatedHistory = { ...existingHistory, @@ -185,13 +173,7 @@ export default function handleChat( setChatHistory([_chatHistory.pop()]); // Move thread to top - if (threadSlug) { - window.dispatchEvent( - new CustomEvent(THREAD_ACTIVITY_EVENT, { - detail: { threadSlug }, - }) - ); - } + dispatchThreadActivityEvent(threadSlug); } // If thread was updated automatically based on chat prompt