Skip to content

Commit 22f907c

Browse files
committed
fix(threads): prevent phantom sessions and restore recency ordering
Remove eager session prewarming that created extra placeholder sessions, and map OpenCode nested time fields so real sessions sort and timestamp correctly. Also timestamp newly started threads immediately so relative time labels render without delay.
1 parent 8b887d3 commit 22f907c

File tree

4 files changed

+45
-39
lines changed

4 files changed

+45
-39
lines changed

src-tauri/src/backend/app_server.rs

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,6 @@ pub(crate) struct WorkspaceSession {
156156
pub(crate) translation_state: Mutex<SessionTranslationState>,
157157
/// Cached model/provider data from `GET /config/providers`.
158158
pub(crate) models_cache: Mutex<Option<Value>>,
159-
/// Pre-warmed OpenCode session ID created eagerly on workspace connect.
160-
/// Consumed by the first `start_thread` call to avoid a duplicate `POST /session`.
161-
pub(crate) prewarmed_session_id: Mutex<Option<String>>,
162159
/// One in-flight prompt at a time per workspace session.
163160
pub(crate) prompt_lock: Mutex<()>,
164161
/// Sender to signal SSE reader shutdown when workspace disconnects.
@@ -634,7 +631,6 @@ pub(crate) async fn spawn_workspace_session<E: EventSink>(
634631
background_thread_callbacks: Mutex::new(HashMap::new()),
635632
translation_state: Mutex::new(SessionTranslationState::new(String::new())),
636633
models_cache: Mutex::new(None),
637-
prewarmed_session_id: Mutex::new(None),
638634
prompt_lock: Mutex::new(()),
639635
shutdown_tx,
640636
});
@@ -656,32 +652,11 @@ pub(crate) async fn spawn_workspace_session<E: EventSink>(
656652
};
657653
event_sink.emit_app_server_event(payload);
658654

659-
// Eagerly create a session and fetch providers to populate the model selector.
655+
// Eagerly fetch providers to populate the model selector.
660656
let prewarm_session = Arc::clone(&session);
661657
let prewarm_sink = event_sink.clone();
662658
let prewarm_workspace_id = entry.id.clone();
663659
tokio::spawn(async move {
664-
// Create a session.
665-
match prewarm_session.rest_post("/session", json!({})).await {
666-
Ok(response) => {
667-
let session_id = response
668-
.get("id")
669-
.and_then(|v| v.as_str())
670-
.unwrap_or_default()
671-
.to_string();
672-
673-
if !session_id.is_empty() {
674-
*prewarm_session.prewarmed_session_id.lock().await = Some(session_id);
675-
}
676-
}
677-
Err(err) => {
678-
eprintln!(
679-
"Pre-warm POST /session failed for {}: {}",
680-
prewarm_workspace_id, err
681-
);
682-
}
683-
}
684-
685660
// Fetch provider/model config.
686661
match prewarm_session.rest_get("/config/providers").await {
687662
Ok(providers) => {

src-tauri/src/shared/codex_core.rs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,6 @@ pub(crate) async fn start_thread_core(
9898
) -> Result<Value, String> {
9999
let session = get_session_clone(sessions, &workspace_id).await?;
100100

101-
// Reuse the pre-warmed session if available.
102-
if let Some(session_id) = session.prewarmed_session_id.lock().await.take() {
103-
let mut ts = session.translation_state.lock().await;
104-
ts.session_id = session_id.clone();
105-
return Ok(json!({
106-
"result": {
107-
"thread": { "id": session_id }
108-
}
109-
}));
110-
}
111-
112101
// POST /session → { id, projectID, directory }
113102
let response = session.rest_post("/session", json!({})).await?;
114103
let session_id = response
@@ -439,11 +428,17 @@ pub(crate) async fn list_threads_core(
439428
let updated_at = s
440429
.get("updatedAt")
441430
.or_else(|| s.get("updated_at"))
431+
.or_else(|| s.get("time").and_then(|time| time.get("updated")))
432+
.or_else(|| s.get("time").and_then(|time| time.get("updatedAt")))
433+
.or_else(|| s.get("time").and_then(|time| time.get("updated_at")))
442434
.cloned()
443435
.unwrap_or(Value::Null);
444436
let created_at = s
445437
.get("createdAt")
446438
.or_else(|| s.get("created_at"))
439+
.or_else(|| s.get("time").and_then(|time| time.get("created")))
440+
.or_else(|| s.get("time").and_then(|time| time.get("createdAt")))
441+
.or_else(|| s.get("time").and_then(|time| time.get("created_at")))
447442
.cloned()
448443
.unwrap_or_else(|| updated_at.clone());
449444
let directory = s

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ describe("useThreadActions", () => {
102102
result: { thread: { id: "thread-1" } },
103103
});
104104

105-
const { result, dispatch, loadedThreadsRef } = renderActions();
105+
const { result, dispatch, loadedThreadsRef, threadActivityRef } =
106+
renderActions();
106107

107108
let threadId: string | null = null;
108109
await act(async () => {
@@ -121,7 +122,23 @@ describe("useThreadActions", () => {
121122
workspaceId: "ws-1",
122123
threadId: "thread-1",
123124
});
125+
expect(dispatch).toHaveBeenCalledWith(
126+
expect.objectContaining({
127+
type: "setThreadTimestamp",
128+
workspaceId: "ws-1",
129+
threadId: "thread-1",
130+
timestamp: expect.any(Number),
131+
}),
132+
);
124133
expect(loadedThreadsRef.current["thread-1"]).toBe(true);
134+
expect(threadActivityRef.current["ws-1"]?.["thread-1"]).toEqual(
135+
expect.any(Number),
136+
);
137+
expect(saveThreadActivity).toHaveBeenCalledWith(
138+
expect.objectContaining({
139+
"ws-1": expect.objectContaining({ "thread-1": expect.any(Number) }),
140+
}),
141+
);
125142
});
126143

127144
it("forks a thread and activates the fork", async () => {

src/features/threads/hooks/useThreadActions.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,26 @@ export function useThreadActions({
103103
});
104104
const threadId = extractThreadId(response);
105105
if (threadId) {
106+
const timestamp = Date.now();
107+
const workspaceActivity = threadActivityRef.current[workspaceId] ?? {};
108+
if (timestamp > (workspaceActivity[threadId] ?? 0)) {
109+
const nextActivity = {
110+
...threadActivityRef.current,
111+
[workspaceId]: {
112+
...workspaceActivity,
113+
[threadId]: timestamp,
114+
},
115+
};
116+
threadActivityRef.current = nextActivity;
117+
saveThreadActivity(nextActivity);
118+
}
106119
dispatch({ type: "ensureThread", workspaceId, threadId });
120+
dispatch({
121+
type: "setThreadTimestamp",
122+
workspaceId,
123+
threadId,
124+
timestamp,
125+
});
107126
if (shouldActivate) {
108127
dispatch({ type: "setActiveThreadId", workspaceId, threadId });
109128
}
@@ -122,7 +141,7 @@ export function useThreadActions({
122141
throw error;
123142
}
124143
},
125-
[dispatch, extractThreadId, loadedThreadsRef, onDebug],
144+
[dispatch, extractThreadId, loadedThreadsRef, onDebug, threadActivityRef],
126145
);
127146

128147
const resumeThreadForWorkspace = useCallback(

0 commit comments

Comments
 (0)