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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 34 additions & 27 deletions src-tauri/src/shared/codex_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,18 @@ use crate::rules;
use crate::shared::account::{build_account_response, read_auth_account};
use crate::types::WorkspaceEntry;

const LOGIN_START_TIMEOUT: Duration = Duration::from_secs(30);
#[allow(dead_code)]
const MAX_INLINE_IMAGE_BYTES: u64 = 50 * 1024 * 1024;
const LOGIN_START_TIMEOUT: Duration = Duration::from_secs(30);
#[allow(dead_code)]
const MAX_INLINE_IMAGE_BYTES: u64 = 50 * 1024 * 1024;
const THREAD_LIST_SOURCE_KINDS: &[&str] = &[
"cli",
"vscode",
"appServer",
"subAgentReview",
"subAgentCompact",
"subAgentThreadSpawn",
"unknown",
];

#[allow(dead_code)]
fn image_mime_type_for_path(path: &str) -> Option<&'static str> {
Expand Down Expand Up @@ -229,33 +238,23 @@ pub(crate) async fn fork_thread_core(
.await
}

pub(crate) async fn list_threads_core(
sessions: &Mutex<HashMap<String, Arc<WorkspaceSession>>>,
workspace_id: String,
cursor: Option<String>,
limit: Option<u32>,
pub(crate) async fn list_threads_core(
sessions: &Mutex<HashMap<String, Arc<WorkspaceSession>>>,
workspace_id: String,
cursor: Option<String>,
limit: Option<u32>,
sort_key: Option<String>,
) -> Result<Value, String> {
let session = get_session_clone(sessions, &workspace_id).await?;
let params = json!({
"cursor": cursor,
"limit": limit,
"sortKey": sort_key,
// Keep interactive and sub-agent sessions visible across CLI versions so
// thread/list refreshes do not drop valid historical conversations.
"sourceKinds": [
"cli",
"vscode",
"appServer",
// Intentionally exclude generic "subAgent" to avoid pulling
// parentless internal sessions (for example memory consolidation).
// Keep only explicit parent-linked sub-agent kinds so internal
// background jobs (for example memory consolidation) stay hidden.
"subAgentReview",
"subAgentCompact",
"subAgentThreadSpawn",
"unknown"
]
let params = json!({
"cursor": cursor,
"limit": limit,
"sortKey": sort_key,
// Keep interactive and sub-agent sessions visible across CLI versions so
// thread/list refreshes do not drop valid historical conversations.
// Intentionally exclude generic "subAgent" so parentless internal jobs
// (for example memory consolidation) do not leak back into app state.
"sourceKinds": THREAD_LIST_SOURCE_KINDS
});
session
.send_request_for_workspace(&workspace_id, "thread/list", params)
Expand Down Expand Up @@ -936,4 +935,12 @@ mod tests {
);
assert_eq!(params.get("serviceTier"), Some(&json!("fast")));
}

#[test]
fn thread_list_source_kinds_exclude_generic_subagent_and_keep_explicit_variants() {
assert!(!THREAD_LIST_SOURCE_KINDS.contains(&"subAgent"));
assert!(THREAD_LIST_SOURCE_KINDS.contains(&"subAgentReview"));
assert!(THREAD_LIST_SOURCE_KINDS.contains(&"subAgentCompact"));
assert!(THREAD_LIST_SOURCE_KINDS.contains(&"subAgentThreadSpawn"));
}
}
17 changes: 17 additions & 0 deletions src/features/app/components/MainApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { useMainAppSidebarMenuOrchestration } from "@app/hooks/useMainAppSidebar
import { useMainAppWorktreeState } from "@app/hooks/useMainAppWorktreeState";
import { useMainAppWorkspaceActions } from "@app/hooks/useMainAppWorkspaceActions";
import { useMainAppWorkspaceLifecycle } from "@app/hooks/useMainAppWorkspaceLifecycle";
import { useHomeAccount } from "@app/hooks/useHomeAccount";
import type {
ComposerEditorSettings,
ServiceTier,
Expand Down Expand Up @@ -1170,6 +1171,20 @@ export default function MainApp() {
const activeRateLimits = activeWorkspaceId
? rateLimitsByWorkspace[activeWorkspaceId] ?? null
: null;
const {
homeAccount,
homeRateLimits,
} = useHomeAccount({
showHome,
usageWorkspaceId,
workspaces,
threadsByWorkspace,
threadListLoadingByWorkspace,
rateLimitsByWorkspace,
accountByWorkspace,
refreshAccountInfo,
refreshAccountRateLimits,
});
const activeTokenUsage = activeThreadId
? tokenUsageByThread[activeThreadId] ?? null
: null;
Expand Down Expand Up @@ -1707,6 +1722,8 @@ export default function MainApp() {
approvals,
activeRateLimits,
activeAccount,
homeRateLimits,
homeAccount,
accountSwitching,
onSwitchAccount: handleSwitchAccount,
onCancelSwitchAccount: handleCancelSwitchAccount,
Expand Down
46 changes: 29 additions & 17 deletions src/features/app/components/Sidebar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { act } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createRef } from "react";
import { Sidebar } from "./Sidebar";
Expand Down Expand Up @@ -72,34 +71,22 @@ const baseProps = {

describe("Sidebar", () => {
it("toggles the search bar from the header icon", () => {
vi.useFakeTimers();
render(<Sidebar {...baseProps} />);

const toggleButton = screen.getByRole("button", { name: "Toggle search" });
expect(screen.queryByLabelText("Search projects")).toBeNull();

act(() => {
fireEvent.click(toggleButton);
});
fireEvent.click(toggleButton);
const input = screen.getByLabelText("Search projects") as HTMLInputElement;
expect(input).toBeTruthy();

act(() => {
fireEvent.change(input, { target: { value: "alpha" } });
vi.runOnlyPendingTimers();
});
fireEvent.change(input, { target: { value: "alpha" } });
expect(input.value).toBe("alpha");

act(() => {
fireEvent.click(toggleButton);
vi.runOnlyPendingTimers();
});
fireEvent.click(toggleButton);
expect(screen.queryByLabelText("Search projects")).toBeNull();

act(() => {
fireEvent.click(toggleButton);
vi.runOnlyPendingTimers();
});
fireEvent.click(toggleButton);
const reopened = screen.getByLabelText("Search projects") as HTMLInputElement;
expect(reopened.value).toBe("");
});
Expand Down Expand Up @@ -141,6 +128,31 @@ describe("Sidebar", () => {
expect(onSetThreadListOrganizeMode).toHaveBeenCalledWith("threads_only");
});

it("renders available credits in the footer when present", () => {
render(
<Sidebar
{...baseProps}
accountRateLimits={{
primary: {
usedPercent: 62,
windowDurationMins: 300,
resetsAt: Math.round(Date.now() / 1000) + 3600,
},
secondary: null,
credits: {
hasCredits: true,
unlimited: false,
balance: "120",
},
planType: "pro",
}}
/>,
);

const creditsLabel = screen.getByText(/^Available credits:/);
expect(creditsLabel.textContent ?? "").toContain("120");
});

it("renders threads-only mode as a global chronological list", () => {
const older = Date.now() - 10_000;
const newer = Date.now();
Expand Down
Loading
Loading