Skip to content

Commit 6ac9563

Browse files
authored
Merge pull request #146 from odefun/feat/refactor-session-settings-909139
refactor: unify CLI session flow and simplify settings domain
2 parents 139fbe6 + ae5e3b3 commit 6ac9563

File tree

23 files changed

+499
-953
lines changed

23 files changed

+499
-953
lines changed

packages/agents/claude/client.ts

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ import {
77
runCliJsonCommand,
88
type SessionEnvironment as RuntimeSessionEnvironment,
99
} from "../runtime/base";
10-
import { getOrCreateThreadSession } from "../runtime/thread-session";
10+
import { createCliThreadSessionManager } from "../runtime/cli-session";
1111
import type {
1212
OpenCodeMessage,
1313
OpenCodeMessageContext,
1414
OpenCodeOptions,
15-
OpenCodeSessionInfo,
1615
} from "../types";
1716

1817
export type SessionEnvironment = RuntimeSessionEnvironment;
@@ -50,52 +49,13 @@ function isValidUuid(value: string): boolean {
5049
return uuidRegex.test(value);
5150
}
5251

53-
export async function createSession(workingPath: string, env?: SessionEnvironment): Promise<string> {
54-
const sessionId = crypto.randomUUID();
55-
runtime.setSessionEnvironment(sessionId, env ?? {});
56-
newSessions.add(sessionId);
57-
log.info("Created Claude session", { sessionId, workingPath });
58-
return sessionId;
59-
}
60-
61-
export async function getOrCreateSession(
62-
channelId: string,
63-
threadId: string,
64-
workingPath: string,
65-
env: SessionEnvironment = {}
66-
): Promise<OpenCodeSessionInfo> {
67-
return getOrCreateThreadSession({
68-
channelId,
69-
threadId,
70-
providerId: "claudecode",
71-
workingPath,
72-
env,
73-
createSession,
74-
getSessionEnvironment: (sessionId) => runtime.getSessionEnvironment(sessionId),
75-
setSessionEnvironment: (sessionId, nextEnv) => {
76-
runtime.setSessionEnvironment(sessionId, nextEnv);
77-
},
78-
validateSessionId: isValidUuid,
79-
onInvalidSessionId: (existingSession) => {
80-
log.info("Invalid Claude session id found; generating new session", {
81-
channelId,
82-
threadId,
83-
workingPath,
84-
existingSession,
85-
});
86-
},
87-
onEnvironmentChanged: () => {
88-
log.info("Claude session environment changed; creating new session", {
89-
channelId,
90-
threadId,
91-
workingPath,
92-
});
93-
},
94-
onCreatingSession: () => {
95-
log.info("Creating new Claude session for thread", { channelId, threadId, workingPath });
96-
},
97-
});
98-
}
52+
export const { createSession, getOrCreateSession } = createCliThreadSessionManager({
53+
providerId: "claudecode",
54+
providerName: "Claude",
55+
runtime,
56+
newSessions,
57+
validateSessionId: isValidUuid,
58+
});
9959

10060
function extractJsonPayload(output: string): string {
10161
const trimmed = output.trim();

packages/agents/codex/client.ts

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@ import {
88
runCliJsonCommand,
99
type SessionEnvironment as RuntimeSessionEnvironment,
1010
} from "../runtime/base";
11-
import { getOrCreateThreadSession } from "../runtime/thread-session";
11+
import { createCliThreadSessionManager } from "../runtime/cli-session";
1212
import type {
1313
OpenCodeMessage,
1414
OpenCodeMessageContext,
1515
OpenCodeOptions,
16-
OpenCodeSessionInfo,
1716
} from "../types";
1817

1918
const runtime = new CliAgentRuntime("Codex");
19+
export const { createSession, getOrCreateSession } = createCliThreadSessionManager({
20+
providerId: "codex",
21+
providerName: "Codex",
22+
runtime,
23+
});
2024

2125
export type SessionEnvironment = RuntimeSessionEnvironment;
2226

@@ -153,43 +157,6 @@ function parseCodexResponse(output: string): {
153157
return { text, threadId };
154158
}
155159

156-
export async function createSession(workingPath: string, env?: SessionEnvironment): Promise<string> {
157-
const sessionId = crypto.randomUUID();
158-
runtime.setSessionEnvironment(sessionId, env ?? {});
159-
log.info("Created Codex session", { sessionId, workingPath });
160-
return sessionId;
161-
}
162-
163-
export async function getOrCreateSession(
164-
channelId: string,
165-
threadId: string,
166-
workingPath: string,
167-
env: SessionEnvironment = {}
168-
): Promise<OpenCodeSessionInfo> {
169-
return getOrCreateThreadSession({
170-
channelId,
171-
threadId,
172-
providerId: "codex",
173-
workingPath,
174-
env,
175-
createSession,
176-
getSessionEnvironment: (sessionId) => runtime.getSessionEnvironment(sessionId),
177-
setSessionEnvironment: (sessionId, nextEnv) => {
178-
runtime.setSessionEnvironment(sessionId, nextEnv);
179-
},
180-
onEnvironmentChanged: () => {
181-
log.info("Codex session environment changed; creating new session", {
182-
channelId,
183-
threadId,
184-
workingPath,
185-
});
186-
},
187-
onCreatingSession: () => {
188-
log.info("Creating new Codex session for thread", { channelId, threadId, workingPath });
189-
},
190-
});
191-
}
192-
193160
export async function sendMessage(
194161
channelId: string,
195162
sessionId: string,

packages/agents/gemini/client.ts

Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ import {
88
runCliJsonCommand,
99
type SessionEnvironment as RuntimeSessionEnvironment,
1010
} from "../runtime/base";
11-
import { getOrCreateThreadSession } from "../runtime/thread-session";
11+
import { createCliThreadSessionManager } from "../runtime/cli-session";
1212
import type {
1313
OpenCodeMessage,
1414
OpenCodeMessageContext,
1515
OpenCodeOptions,
16-
OpenCodeSessionInfo,
1716
} from "../types";
1817

1918
export type SessionEnvironment = RuntimeSessionEnvironment;
@@ -39,6 +38,12 @@ type GeminiJsonRecord = {
3938

4039
const runtime = new CliAgentRuntime("Gemini");
4140
const newSessions = new Set<string>();
41+
export const { createSession, getOrCreateSession } = createCliThreadSessionManager({
42+
providerId: "gemini",
43+
providerName: "Gemini",
44+
runtime,
45+
newSessions,
46+
});
4247

4348
function resolveGeminiBinary(): string {
4449
if (typeof Bun !== "undefined" && Bun.which("gemini")) return "gemini";
@@ -158,44 +163,6 @@ function parseGeminiResponse(output: string): {
158163
return { text, sessionId };
159164
}
160165

161-
export async function createSession(workingPath: string, env?: SessionEnvironment): Promise<string> {
162-
const sessionId = crypto.randomUUID();
163-
runtime.setSessionEnvironment(sessionId, env ?? {});
164-
newSessions.add(sessionId);
165-
log.info("Created Gemini session", { sessionId, workingPath });
166-
return sessionId;
167-
}
168-
169-
export async function getOrCreateSession(
170-
channelId: string,
171-
threadId: string,
172-
workingPath: string,
173-
env: SessionEnvironment = {}
174-
): Promise<OpenCodeSessionInfo> {
175-
return getOrCreateThreadSession({
176-
channelId,
177-
threadId,
178-
providerId: "gemini",
179-
workingPath,
180-
env,
181-
createSession,
182-
getSessionEnvironment: (sessionId) => runtime.getSessionEnvironment(sessionId),
183-
setSessionEnvironment: (sessionId, nextEnv) => {
184-
runtime.setSessionEnvironment(sessionId, nextEnv);
185-
},
186-
onEnvironmentChanged: () => {
187-
log.info("Gemini session environment changed; creating new session", {
188-
channelId,
189-
threadId,
190-
workingPath,
191-
});
192-
},
193-
onCreatingSession: () => {
194-
log.info("Creating new Gemini session for thread", { channelId, threadId, workingPath });
195-
},
196-
});
197-
}
198-
199166
export async function sendMessage(
200167
channelId: string,
201168
sessionId: string,

packages/agents/goose/client.ts

Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ import {
88
runCliJsonCommand,
99
type SessionEnvironment as RuntimeSessionEnvironment,
1010
} from "../runtime/base";
11-
import { getOrCreateThreadSession } from "../runtime/thread-session";
11+
import { createCliThreadSessionManager } from "../runtime/cli-session";
1212
import type {
1313
OpenCodeMessage,
1414
OpenCodeMessageContext,
1515
OpenCodeOptions,
16-
OpenCodeSessionInfo,
1716
} from "../types";
1817

1918
export type SessionEnvironment = RuntimeSessionEnvironment;
@@ -43,6 +42,12 @@ type GooseJsonRecord = {
4342

4443
const runtime = new CliAgentRuntime("Goose");
4544
const newSessions = new Set<string>();
45+
export const { createSession, getOrCreateSession } = createCliThreadSessionManager({
46+
providerId: "goose",
47+
providerName: "Goose",
48+
runtime,
49+
newSessions,
50+
});
4651

4752
function resolveGooseBinary(): string {
4853
if (typeof Bun !== "undefined" && Bun.which("goose")) return "goose";
@@ -192,44 +197,6 @@ export function parseGooseResponse(output: string): {
192197
return { text, sessionId };
193198
}
194199

195-
export async function createSession(workingPath: string, env?: SessionEnvironment): Promise<string> {
196-
const sessionId = crypto.randomUUID();
197-
runtime.setSessionEnvironment(sessionId, env ?? {});
198-
newSessions.add(sessionId);
199-
log.info("Created Goose session", { sessionId, workingPath });
200-
return sessionId;
201-
}
202-
203-
export async function getOrCreateSession(
204-
channelId: string,
205-
threadId: string,
206-
workingPath: string,
207-
env: SessionEnvironment = {}
208-
): Promise<OpenCodeSessionInfo> {
209-
return getOrCreateThreadSession({
210-
channelId,
211-
threadId,
212-
providerId: "goose",
213-
workingPath,
214-
env,
215-
createSession,
216-
getSessionEnvironment: (sessionId) => runtime.getSessionEnvironment(sessionId),
217-
setSessionEnvironment: (sessionId, nextEnv) => {
218-
runtime.setSessionEnvironment(sessionId, nextEnv);
219-
},
220-
onEnvironmentChanged: () => {
221-
log.info("Goose session environment changed; creating new session", {
222-
channelId,
223-
threadId,
224-
workingPath,
225-
});
226-
},
227-
onCreatingSession: () => {
228-
log.info("Creating new Goose session for thread", { channelId, threadId, workingPath });
229-
},
230-
});
231-
}
232-
233200
export async function sendMessage(
234201
channelId: string,
235202
sessionId: string,

packages/agents/goose/session-state.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,40 @@ export type GooseInspectorToolState = StreamToolState;
5959

6060
export type GooseStreamStateMaps = StreamStateMaps<GooseInspectorToolState>;
6161

62+
function resolveGooseToolResponseId(block: {
63+
id?: string;
64+
tool_use_id?: string;
65+
}): string {
66+
if (typeof block.id === "string" && block.id.trim()) {
67+
return block.id;
68+
}
69+
if (typeof block.tool_use_id === "string" && block.tool_use_id.trim()) {
70+
return block.tool_use_id;
71+
}
72+
return "";
73+
}
74+
75+
function extractGooseToolResponseText(value: unknown): string {
76+
if (typeof value === "string") {
77+
return value.trim();
78+
}
79+
if (!Array.isArray(value)) {
80+
return "";
81+
}
82+
return value
83+
.filter((entry) => entry && typeof entry === "object")
84+
.map((entry) => {
85+
const record = entry as { type?: string; text?: string };
86+
if (record.type === "text" && typeof record.text === "string") {
87+
return record.text;
88+
}
89+
return "";
90+
})
91+
.filter((text) => text.length > 0)
92+
.join("\n")
93+
.trim();
94+
}
95+
6296
function parseTodosFromGooseToolInput(toolName: string, input: Record<string, unknown> | undefined): SessionTodo[] | undefined {
6397
const direct = parseTodosFromToolInput(toolName, input);
6498
if (direct) return direct;
@@ -97,6 +131,9 @@ export function applyGooseRecordToState(
97131
const blocks = record.message?.content ?? [];
98132

99133
if (role === "assistant") {
134+
const messageCreatedAtMs = typeof record.message?.created === "number"
135+
? record.message.created * 1000
136+
: Date.now();
100137
for (const block of blocks) {
101138
if (block?.type === "text") {
102139
const chunk = typeof block.text === "string" ? block.text : "";
@@ -138,7 +175,12 @@ export function applyGooseRecordToState(
138175
output: existing?.output,
139176
error: existing?.error,
140177
title: existing?.title,
141-
metadata: existing?.metadata,
178+
metadata: {
179+
...(existing?.metadata ?? {}),
180+
startedAtMs: typeof existing?.metadata?.startedAtMs === "number"
181+
? existing.metadata.startedAtMs
182+
: messageCreatedAtMs,
183+
},
142184
};
143185
toolById.set(callId, tool);
144186
updateTool(state, tool);
@@ -150,16 +192,12 @@ export function applyGooseRecordToState(
150192
if (role === "user") {
151193
for (const block of blocks) {
152194
if (block?.type !== "toolResponse") continue;
153-
const callId = typeof block.id === "string" && block.id.trim() ? block.id : "";
195+
const callId = resolveGooseToolResponseId(block);
154196
if (!callId) continue;
155197
const existing = toolById.get(callId);
156198
if (!existing) continue;
157199
const result = block.toolResult?.value;
158-
const output = (result?.content ?? [])
159-
.filter((entry) => entry?.type === "text")
160-
.map((entry) => entry.text ?? "")
161-
.join("\n")
162-
.trim();
200+
const output = extractGooseToolResponseText(result?.content);
163201
const hasError = result?.isError === true || block.toolResult?.status === "error";
164202
const updated: GooseInspectorToolState = {
165203
...existing,

0 commit comments

Comments
 (0)