Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/pr-assets/workspace-session-tree-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions crates/agent-gateway/web/src/app/GatewayAppView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
import type { PendingUploadedFile } from "@liveagent/ui/lib/chat/uploadedFiles";
import { mergePendingUploadedFiles } from "@liveagent/ui/lib/chat/uploadedFiles";
import { cn } from "@liveagent/ui/lib/shared/utils";
import { setSidebarConversationArchived } from "@liveagent/ui/lib/sidebar/preferences";
import { useSidebarSelector } from "@liveagent/ui/lib/sidebar/useSidebarSelector";
import {
mergeTerminalSession,
Expand Down Expand Up @@ -1269,6 +1270,24 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel }

<div className="gateway-editor-host">
<GatewaySidebarContainer
pinnedOrder={settings.system.sidebarPinnedOrder}
onReorderPinned={(sidebarPinnedOrder) =>
setSettings((previous) => ({
...previous,
system: { ...previous.system, sidebarPinnedOrder },
}))
}
projectOrder={settings.system.workspaceProjectOrder}
onReorderProjects={(workspaceProjectOrder) =>
setSettings((previous) => ({
...previous,
system: { ...previous.system, workspaceProjectOrder },
}))
}
archivedConversations={settings.system.archivedConversations}
onSetConversationArchived={(item, archived) =>
setSettings((previous) => setSidebarConversationArchived(previous, item, archived))
}
store={sidebarStore}
approvalConversationIds={approvalConversationIds}
transientRunningConversations={manualCompactTransientConversations}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import {
} from "@liveagent/ui/components/chat/ChatHistorySidebar";
import { useLocale } from "@liveagent/ui/i18n/index";
import type { SidebarBatchDeleteOptions } from "@liveagent/ui/lib/sidebar/batchDelete";
import { deleteSidebarConversations } from "@liveagent/ui/lib/sidebar/batchDelete";
import {
deleteSidebarConversation,
deleteSidebarConversations,
} from "@liveagent/ui/lib/sidebar/batchDelete";
import type { SidebarSnapshot, SidebarStore } from "@liveagent/ui/lib/sidebar/store";
import type { TransientSidebarRunningConversation } from "@liveagent/ui/lib/sidebar/transientActivity";
import { mergeTransientSidebarRunningActivity } from "@liveagent/ui/lib/sidebar/transientActivity";
Expand Down Expand Up @@ -124,7 +127,8 @@ export function GatewaySidebarContainer(props: GatewaySidebarContainerProps) {
mutations,
mutationErrors,
projectActivityInputs,
} = useSidebarContainerState(store);
workspaceHistory,
} = useSidebarContainerState(store, props.showProjects);
const conversationIndex = useSidebarSelector(store, selectConversationIndex);
const effectiveRunningActivity = useMemo(
() =>
Expand Down Expand Up @@ -231,7 +235,7 @@ export function GatewaySidebarContainer(props: GatewaySidebarContainerProps) {
props.onLocalDraftDeleted(id);
return;
}
void store.remove(id);
void deleteSidebarConversation(id, props);
});

const handleDeleteConversations = useStableCallback(
Expand All @@ -249,7 +253,7 @@ export function GatewaySidebarContainer(props: GatewaySidebarContainerProps) {
props.onLocalDraftDeleted(id);
return true;
}
return store.remove(id);
return deleteSidebarConversation(id, props);
},
options,
);
Expand Down Expand Up @@ -319,18 +323,22 @@ export function GatewaySidebarContainer(props: GatewaySidebarContainerProps) {
const sortedProjects = useMemo(
() =>
sortWorkspaceProjectsByActivity(projects, {
projectOrder: props.projectOrder,
projectActivityUpdatedAts: projectActivityInputs.workdirActivity,
runningProjectPathKeys: effectiveRunningActivity.runningProjectPathKeys,
}),
[
effectiveRunningActivity.runningProjectPathKeys,
projectActivityInputs.workdirActivity,
projects,
props.projectOrder,
],
);

return (
<ChatHistorySidebar
workspaceHistory={workspaceHistory}
onLoadWorkspaceHistory={store.loadWorkspaceHistory}
{...buildChatHistorySidebarBaseProps(props, {
items,
busyConversationIds: mutations,
Expand Down
9 changes: 9 additions & 0 deletions crates/agent-gateway/web/src/lib/sidebar/webSidebarBackend.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { listPinnedSidebarConversations } from "@liveagent/ui/lib/sidebar/pinnedHistory";
// Web-only SidebarBackend adapter over the gateway WebSocket client. NOT
// byte-mirrored: everything gateway-specific (snake_case summaries, epoch
// units, the activity-store bridge) is normalized here so the mirrored store
Expand Down Expand Up @@ -154,6 +155,14 @@ export function createWebSidebarBackend(deps: WebSidebarBackendDeps): SidebarBac
const { api, activityStore } = deps;

return {
listPinnedConversations: () =>
listPinnedSidebarConversations(async (page, pageSize) => {
const response = await api.listHistory(page, pageSize);
return {
items: response.conversations.map(normalizeGatewayConversationSummary),
totalCount: response.total_count,
};
}),
async listConversations(page, pageSize, scope): Promise<SidebarListPage> {
const filter = scopeToHistoryListFilter(scope);
if (!filter) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ const SYSTEM_COMMAND_SAFETY_MODE_KEY: &str = "commandSafetyMode";
const SYSTEM_BROWSER_AUTOMATION_MODE_KEY: &str = "browserAutomationMode";
const SYSTEM_WORKSPACE_PROJECTS_KEY: &str = "workspaceProjects";
const SYSTEM_WORKSPACE_PROJECT_GROUPS_KEY: &str = "workspaceProjectGroups";
const SYSTEM_WORKSPACE_PROJECT_ORDER_KEY: &str = "workspaceProjectOrder";
const SYSTEM_SIDEBAR_PINNED_ORDER_KEY: &str = "sidebarPinnedOrder";
const SYSTEM_ARCHIVED_CONVERSATIONS_KEY: &str = "archivedConversations";
const SYSTEM_ACTIVE_WORKSPACE_PROJECT_ID_KEY: &str = "activeWorkspaceProjectId";
const SYSTEM_HIDDEN_WORKSPACE_PROJECT_PATHS_KEY: &str = "hiddenWorkspaceProjectPaths";
const SYSTEM_MISSING_WORKSPACE_PROJECT_PATHS_KEY: &str = "missingWorkspaceProjectPaths";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,17 @@ fn save_system_with_default_workdir(
SYSTEM_WORKSPACE_RESOURCE_SETTINGS_KEY,
SYSTEM_SYSTEM_PROXY_KEY,
SYSTEM_CUA_ALLOW_SELF_TARGETING_KEY,
] {
]
.into_iter()
.chain(
[
SYSTEM_WORKSPACE_PROJECT_ORDER_KEY,
SYSTEM_SIDEBAR_PINNED_ORDER_KEY,
SYSTEM_ARCHIVED_CONVERSATIONS_KEY,
]
.into_iter()
.filter(|key| system.contains_key(*key)),
) {
let value = system.get(key).cloned().unwrap_or(Value::Null);
tx.execute(
SYSTEM_SETTINGS_INSERT_SQL,
Expand Down
45 changes: 45 additions & 0 deletions crates/agent-gui/src-tauri/src/commands/config/settings/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,51 @@ mod tests {
.expect("collect table columns")
}

#[test]
fn save_system_preserves_sidebar_order_across_database_reopen() {
let temp = tempfile::tempdir().expect("temp database");
let path = temp.path().join("settings.sqlite");
let order = json!(["/workspace/b", "/workspace/a"]);
let pins = json!([
"workspace:/workspace/a",
"conversation:one",
"workspace:/workspace/b"
]);
let archive =
json!([{ "id": "archived", "title": "Archived conversation", "cwd": "/workspace/a" }]);
{
let mut conn = Connection::open(&path).expect("open database");
initialize_schema(&conn).expect("initialize database");
save_system_with_default_workdir(
&mut conn,
json!({
"workspaceProjectOrder": order,
"sidebarPinnedOrder": pins,
"archivedConversations": archive,
}),
"/workspace/default",
)
.expect("save sidebar preferences");
}
let mut conn = Connection::open(&path).expect("reopen database");
let mut loaded =
load_system_with_defaults(&conn, "/workspace/default").expect("load preferences");
assert_eq!(loaded.get(SYSTEM_WORKSPACE_PROJECT_ORDER_KEY), Some(&order));
assert_eq!(loaded.get(SYSTEM_SIDEBAR_PINNED_ORDER_KEY), Some(&pins));
assert_eq!(
loaded.get(SYSTEM_ARCHIVED_CONVERSATIONS_KEY),
Some(&archive)
);
loaded["executionMode"] = json!("tools");
save_system_with_default_workdir(&mut conn, loaded, "/workspace/default")
.expect("save unrelated setting");
let loaded = load_system(&conn)
.expect("reload preferences")
.expect("system settings");
assert_eq!(loaded.get(SYSTEM_SIDEBAR_PINNED_ORDER_KEY), Some(&pins));
assert_eq!(loaded.get(SYSTEM_WORKSPACE_PROJECT_ORDER_KEY), Some(&order));
}

#[test]
fn initialize_schema_creates_all_tables() {
let conn = open_memory_db();
Expand Down
3 changes: 3 additions & 0 deletions crates/agent-gui/src/lib/sidebar/guiSidebarBackend.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { listPinnedSidebarConversations } from "@liveagent/ui/lib/sidebar/pinnedHistory";
// GUI adapter for the shared sidebar state layer: wraps the Tauri chat-history
// IPC surface and the single CHAT_HISTORY_SYNC_EVENT subscription. This file
// is NOT mirrored — it is the desktop end's platform boundary.
Expand Down Expand Up @@ -61,6 +62,8 @@ function toSidebarBackendEvent(event: ChatHistorySyncEvent): SidebarBackendEvent

export function createGuiSidebarBackend(): SidebarBackend {
return {
listPinnedConversations: () =>
listPinnedSidebarConversations((page, pageSize) => listChatHistory(page, pageSize)),
// scope.kind === "none" never reaches the adapter — the store resolves it
// locally to an empty list without an IPC round-trip.
listConversations: async (page, pageSize, scope) => {
Expand Down
19 changes: 19 additions & 0 deletions crates/agent-gui/src/pages/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
type ConversationOpenState,
createConversationOpenController,
} from "@liveagent/ui/lib/sidebar/openController";
import { setSidebarConversationArchived } from "@liveagent/ui/lib/sidebar/preferences";
import { conversationMatchesScope } from "@liveagent/ui/lib/sidebar/scope";
import {
selectConversations,
Expand Down Expand Up @@ -3917,6 +3918,24 @@ export function ChatPage(props: ChatPageProps) {
{workbenchDragGhost}
{/* ---- Left column: navigation/sidebar ---- */}
<ChatSidebarContainer
pinnedOrder={settings.system.sidebarPinnedOrder}
onReorderPinned={(sidebarPinnedOrder) =>
setSettings((previous) => ({
...previous,
system: { ...previous.system, sidebarPinnedOrder },
}))
}
projectOrder={settings.system.workspaceProjectOrder}
onReorderProjects={(workspaceProjectOrder) =>
setSettings((previous) => ({
...previous,
system: { ...previous.system, workspaceProjectOrder },
}))
}
archivedConversations={settings.system.archivedConversations}
onSetConversationArchived={(item, archived) =>
setSettings((previous) => setSidebarConversationArchived(previous, item, archived))
}
store={sidebarStore}
approvalStore={conversationRuntimeRegistry.approvals}
questionStore={conversationRuntimeRegistry.questions}
Expand Down
Loading
Loading