Skip to content

Commit 9d217a5

Browse files
committed
fix: restore updater/menu wiring and thread session loading
1 parent f4b8e51 commit 9d217a5

12 files changed

Lines changed: 322 additions & 31 deletions

File tree

src-tauri/src/backend/app_server.rs

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ fn normalize_root_path(value: &str) -> String {
4949
if normalized.is_empty() {
5050
return String::new();
5151
}
52+
let lower = normalized.to_ascii_lowercase();
53+
let normalized = if lower.starts_with("//?/unc/") {
54+
format!("//{}", &normalized[8..])
55+
} else if lower.starts_with("//?/") || lower.starts_with("//./") {
56+
normalized[4..].to_string()
57+
} else {
58+
normalized.to_string()
59+
};
60+
if normalized.is_empty() {
61+
return String::new();
62+
}
5263

5364
let bytes = normalized.as_bytes();
5465
let is_drive_path = bytes.len() >= 3
@@ -133,13 +144,24 @@ fn resolve_workspace_for_cwd(
133144
if normalized_cwd.is_empty() {
134145
return None;
135146
}
136-
workspace_roots.iter().find_map(|(workspace_id, root)| {
137-
if root == &normalized_cwd {
138-
Some(workspace_id.clone())
139-
} else {
140-
None
141-
}
142-
})
147+
workspace_roots
148+
.iter()
149+
.filter_map(|(workspace_id, root)| {
150+
if root.is_empty() {
151+
return None;
152+
}
153+
let is_exact_match = root == &normalized_cwd;
154+
let is_nested_match = normalized_cwd.len() > root.len()
155+
&& normalized_cwd.starts_with(root)
156+
&& normalized_cwd.as_bytes().get(root.len()) == Some(&b'/');
157+
if is_exact_match || is_nested_match {
158+
Some((workspace_id, root.len()))
159+
} else {
160+
None
161+
}
162+
})
163+
.max_by_key(|(_, root_len)| *root_len)
164+
.map(|(workspace_id, _)| workspace_id.clone())
143165
}
144166

145167
fn is_global_workspace_notification(method: &str) -> bool {
@@ -844,4 +866,46 @@ mod tests {
844866
Some("ws-1".to_string())
845867
);
846868
}
869+
870+
#[test]
871+
fn resolve_workspace_for_cwd_normalizes_windows_namespace_paths() {
872+
let mut roots = HashMap::new();
873+
roots.insert("ws-1".to_string(), normalize_root_path("C:\\Dev\\Codex"));
874+
assert_eq!(
875+
resolve_workspace_for_cwd("\\\\?\\C:\\Dev\\Codex", &roots),
876+
Some("ws-1".to_string())
877+
);
878+
}
879+
880+
#[test]
881+
fn normalize_root_path_normalizes_windows_namespace_unc_paths() {
882+
assert_eq!(
883+
normalize_root_path("\\\\?\\UNC\\SERVER\\Share\\Repo\\"),
884+
"//server/share/repo"
885+
);
886+
}
887+
888+
#[test]
889+
fn resolve_workspace_for_cwd_matches_nested_paths() {
890+
let mut roots = HashMap::new();
891+
roots.insert("ws-1".to_string(), normalize_root_path("/tmp/codex"));
892+
assert_eq!(
893+
resolve_workspace_for_cwd("/tmp/codex/subdir/project", &roots),
894+
Some("ws-1".to_string())
895+
);
896+
}
897+
898+
#[test]
899+
fn resolve_workspace_for_cwd_prefers_longest_matching_root() {
900+
let mut roots = HashMap::new();
901+
roots.insert("ws-parent".to_string(), normalize_root_path("/tmp/codex"));
902+
roots.insert(
903+
"ws-child".to_string(),
904+
normalize_root_path("/tmp/codex/subdir"),
905+
);
906+
assert_eq!(
907+
resolve_workspace_for_cwd("/tmp/codex/subdir/project", &roots),
908+
Some("ws-child".to_string())
909+
);
910+
}
847911
}

src-tauri/src/lib.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,7 @@ pub fn run() {
8888
}
8989
}
9090

91-
// Windows: no native menu bar.
92-
#[cfg(all(desktop, target_os = "windows"))]
93-
let builder = tauri::Builder::default().manage(menu::MenuItemRegistry::<tauri::Wry>::default());
94-
95-
// macOS/Linux: keep the native menu behavior.
96-
#[cfg(all(desktop, not(target_os = "windows")))]
91+
#[cfg(desktop)]
9792
let builder = tauri::Builder::default()
9893
.manage(menu::MenuItemRegistry::<tauri::Wry>::default())
9994
.on_menu_event(menu::handle_menu_event)
@@ -121,6 +116,8 @@ pub fn run() {
121116
{
122117
if let Some(main_window) = app.get_webview_window("main") {
123118
let _ = main_window.set_decorations(false);
119+
// Keep menu accelerators wired while suppressing a visible native menu bar.
120+
let _ = main_window.hide_menu();
124121
}
125122
}
126123
#[cfg(desktop)]

src-tauri/src/menu.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ use std::collections::HashMap;
22
use std::sync::Mutex;
33

44
use serde::Deserialize;
5-
#[cfg(not(target_os = "windows"))]
65
use tauri::menu::{Menu, MenuItemBuilder, PredefinedMenuItem, Submenu};
76
use tauri::menu::MenuItem;
8-
#[cfg(not(target_os = "windows"))]
97
use tauri::{Emitter, WebviewUrl, WebviewWindowBuilder};
108
use tauri::{Manager, Runtime};
119

@@ -22,7 +20,6 @@ impl<R: Runtime> Default for MenuItemRegistry<R> {
2220
}
2321

2422
impl<R: Runtime> MenuItemRegistry<R> {
25-
#[cfg(not(target_os = "windows"))]
2623
fn register(&self, id: &str, item: &MenuItem<R>) {
2724
if let Ok(mut items) = self.items.lock() {
2825
items.insert(id.to_string(), item.clone());
@@ -63,7 +60,6 @@ pub fn menu_set_accelerators<R: Runtime>(
6360
Ok(())
6461
}
6562

66-
#[cfg(not(target_os = "windows"))]
6763
pub(crate) fn build_menu<R: tauri::Runtime>(
6864
handle: &tauri::AppHandle<R>,
6965
) -> tauri::Result<Menu<R>> {
@@ -328,7 +324,6 @@ pub(crate) fn build_menu<R: tauri::Runtime>(
328324
)
329325
}
330326

331-
#[cfg(not(target_os = "windows"))]
332327
pub(crate) fn handle_menu_event<R: tauri::Runtime>(
333328
app: &tauri::AppHandle<R>,
334329
event: tauri::menu::MenuEvent,
@@ -396,7 +391,6 @@ pub(crate) fn handle_menu_event<R: tauri::Runtime>(
396391
}
397392
}
398393

399-
#[cfg(not(target_os = "windows"))]
400394
fn emit_menu_event<R: tauri::Runtime>(app: &tauri::AppHandle<R>, event: &str) {
401395
if let Some(window) = app.get_webview_window("main") {
402396
let _ = window.show();

src-tauri/src/shared/codex_core.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,19 @@ pub(crate) async fn list_threads_core(
241241
"cursor": cursor,
242242
"limit": limit,
243243
"sortKey": sort_key,
244-
// Keep spawned sub-agent sessions visible in thread/list so UI refreshes
245-
// do not drop parent -> child sidebar relationships.
246-
"sourceKinds": ["cli", "vscode", "subAgentThreadSpawn"]
244+
// Keep interactive and sub-agent sessions visible across CLI versions so
245+
// thread/list refreshes do not drop valid historical conversations.
246+
"sourceKinds": [
247+
"cli",
248+
"vscode",
249+
"appServer",
250+
"subAgent",
251+
"subAgentReview",
252+
"subAgentCompact",
253+
"subAgentThreadSpawn",
254+
"subAgentOther",
255+
"unknown"
256+
]
247257
});
248258
session
249259
.send_request_for_workspace(&workspace_id, "thread/list", params)

src/features/settings/components/sections/SettingsAboutSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function formatBytes(value: number) {
1919
export function SettingsAboutSection() {
2020
const [appBuildType, setAppBuildType] = useState<AppBuildType | "unknown">("unknown");
2121
const { state: updaterState, checkForUpdates, startUpdate } = useUpdater({
22-
enabled: false,
22+
enabled: true,
2323
});
2424

2525
useEffect(() => {

src/features/threads/hooks/threadReducer/threadLifecycleSlice.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,31 @@ export function reduceThreadLifecycle(
312312
case "setThreads": {
313313
const hidden = state.hiddenThreadIdsByWorkspace[action.workspaceId] ?? {};
314314
const visibleThreads = action.threads.filter((thread) => !hidden[thread.id]);
315+
const preserveAnchors = action.preserveAnchors === true;
316+
if (!preserveAnchors) {
317+
const currentActiveThreadId =
318+
state.activeThreadIdByWorkspace[action.workspaceId] ?? null;
319+
const activeThreadStillVisible = currentActiveThreadId
320+
? visibleThreads.some((thread) => thread.id === currentActiveThreadId)
321+
: false;
322+
return {
323+
...state,
324+
threadsByWorkspace: {
325+
...state.threadsByWorkspace,
326+
[action.workspaceId]: visibleThreads,
327+
},
328+
activeThreadIdByWorkspace: {
329+
...state.activeThreadIdByWorkspace,
330+
[action.workspaceId]: activeThreadStillVisible
331+
? currentActiveThreadId
332+
: (visibleThreads[0]?.id ?? null),
333+
},
334+
threadSortKeyByWorkspace: {
335+
...state.threadSortKeyByWorkspace,
336+
[action.workspaceId]: action.sortKey,
337+
},
338+
};
339+
}
315340
const existingThreads = state.threadsByWorkspace[action.workspaceId] ?? [];
316341
const existingById = new Map(
317342
existingThreads.map((thread) => [thread.id, thread] as const),

src/features/threads/hooks/useThreadActions.test.tsx

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,7 @@ describe("useThreadActions", () => {
734734
type: "setThreads",
735735
workspaceId: "ws-1",
736736
sortKey: "updated_at",
737+
preserveAnchors: true,
737738
threads: [
738739
{
739740
id: "thread-1",
@@ -1095,6 +1096,84 @@ describe("useThreadActions", () => {
10951096
});
10961097
});
10971098

1099+
it("matches windows namespace-prefixed workspace threads client-side", async () => {
1100+
const windowsWorkspace: WorkspaceInfo = {
1101+
...workspace,
1102+
path: "C:\\Dev\\CodexMon",
1103+
};
1104+
vi.mocked(listThreads).mockResolvedValue({
1105+
result: {
1106+
data: [
1107+
{
1108+
id: "thread-win-ns-1",
1109+
cwd: "\\\\?\\C:\\Dev\\CodexMon",
1110+
preview: "Windows namespace thread",
1111+
updated_at: 5000,
1112+
},
1113+
],
1114+
nextCursor: null,
1115+
},
1116+
});
1117+
vi.mocked(getThreadTimestamp).mockReturnValue(5000);
1118+
1119+
const { result, dispatch } = renderActions();
1120+
1121+
await act(async () => {
1122+
await result.current.listThreadsForWorkspace(windowsWorkspace);
1123+
});
1124+
1125+
expect(dispatch).toHaveBeenCalledWith({
1126+
type: "setThreads",
1127+
workspaceId: "ws-1",
1128+
sortKey: "updated_at",
1129+
threads: [
1130+
{
1131+
id: "thread-win-ns-1",
1132+
name: "Windows namespace thread",
1133+
updatedAt: 5000,
1134+
createdAt: 0,
1135+
},
1136+
],
1137+
});
1138+
});
1139+
1140+
it("matches nested workspace threads client-side", async () => {
1141+
vi.mocked(listThreads).mockResolvedValue({
1142+
result: {
1143+
data: [
1144+
{
1145+
id: "thread-nested-1",
1146+
cwd: "/tmp/codex/subdir/project",
1147+
preview: "Nested thread",
1148+
updated_at: 5000,
1149+
},
1150+
],
1151+
nextCursor: null,
1152+
},
1153+
});
1154+
vi.mocked(getThreadTimestamp).mockReturnValue(5000);
1155+
1156+
const { result, dispatch } = renderActions();
1157+
1158+
await act(async () => {
1159+
await result.current.listThreadsForWorkspace(workspace);
1160+
});
1161+
1162+
expect(dispatch).toHaveBeenCalledWith({
1163+
type: "setThreads",
1164+
workspaceId: "ws-1",
1165+
sortKey: "updated_at",
1166+
threads: [
1167+
{
1168+
id: "thread-nested-1",
1169+
name: "Nested thread",
1170+
updatedAt: 5000,
1171+
createdAt: 0,
1172+
},
1173+
],
1174+
});
1175+
});
1176+
10981177
it("preserves list state when requested", async () => {
10991178
vi.mocked(listThreads).mockResolvedValue({
11001179
result: {
@@ -1313,6 +1392,52 @@ describe("useThreadActions", () => {
13131392
});
13141393
});
13151394

1395+
it("matches nested workspace threads when loading older threads", async () => {
1396+
vi.mocked(listThreads).mockResolvedValue({
1397+
result: {
1398+
data: [
1399+
{
1400+
id: "thread-nested-older",
1401+
cwd: "/tmp/codex/subdir/project",
1402+
preview: "Nested older preview",
1403+
updated_at: 4000,
1404+
},
1405+
],
1406+
nextCursor: null,
1407+
},
1408+
});
1409+
vi.mocked(getThreadTimestamp).mockImplementation((thread) => {
1410+
const value = (thread as Record<string, unknown>).updated_at as number;
1411+
return value ?? 0;
1412+
});
1413+
1414+
const { result, dispatch } = renderActions({
1415+
threadsByWorkspace: {
1416+
"ws-1": [{ id: "thread-1", name: "Agent 1", updatedAt: 6000 }],
1417+
},
1418+
threadListCursorByWorkspace: { "ws-1": "cursor-1" },
1419+
});
1420+
1421+
await act(async () => {
1422+
await result.current.loadOlderThreadsForWorkspace(workspace);
1423+
});
1424+
1425+
expect(dispatch).toHaveBeenCalledWith({
1426+
type: "setThreads",
1427+
workspaceId: "ws-1",
1428+
sortKey: "updated_at",
1429+
threads: [
1430+
{ id: "thread-1", name: "Agent 1", updatedAt: 6000 },
1431+
{
1432+
id: "thread-nested-older",
1433+
name: "Nested older preview",
1434+
updatedAt: 4000,
1435+
createdAt: 0,
1436+
},
1437+
],
1438+
});
1439+
});
1440+
13161441
it("detects model metadata from list responses", async () => {
13171442
vi.mocked(listThreads).mockResolvedValue({
13181443
result: {

0 commit comments

Comments
 (0)