mirror of
https://github.com/block/goose.git
synced 2026-04-28 03:29:36 +00:00
feat: migrate session metadata storage from frontend overlay to backend (#8769)
Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Bradley Axen <baxen@squareup.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
97671f30b0
commit
86afdeab6a
19 changed files with 287 additions and 411 deletions
|
|
@ -222,6 +222,15 @@ pub struct UpdateSessionProjectRequest {
|
|||
pub project_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Rename a session.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/session/rename", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RenameSessionRequest {
|
||||
pub session_id: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
/// Archive a session (soft delete).
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/session/archive", response = EmptyResponse)]
|
||||
|
|
|
|||
|
|
@ -110,6 +110,11 @@
|
|||
"requestType": "UpdateSessionProjectRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/session/rename",
|
||||
"requestType": "RenameSessionRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/session/archive",
|
||||
"requestType": "ArchiveSessionRequest",
|
||||
|
|
|
|||
|
|
@ -743,6 +743,24 @@
|
|||
"x-side": "agent",
|
||||
"x-method": "_goose/session/update_project"
|
||||
},
|
||||
"RenameSessionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sessionId",
|
||||
"title"
|
||||
],
|
||||
"description": "Rename a session.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/session/rename"
|
||||
},
|
||||
"ArchiveSessionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1582,6 +1600,15 @@
|
|||
"description": "Params for _goose/session/update_project",
|
||||
"title": "UpdateSessionProjectRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/RenameSessionRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/session/rename",
|
||||
"title": "RenameSessionRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -171,20 +171,51 @@ fn sid_short(id: &str) -> String {
|
|||
}
|
||||
|
||||
fn thread_session_meta(
|
||||
message_count: i64,
|
||||
metadata: &crate::session::ThreadMetadata,
|
||||
thread: &crate::session::Thread,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
let mut meta = serde_json::Map::new();
|
||||
meta.insert(
|
||||
"messageCount".to_string(),
|
||||
serde_json::Value::Number(message_count.into()),
|
||||
serde_json::Value::Number(thread.message_count.into()),
|
||||
);
|
||||
if let Some(ref pid) = metadata.project_id {
|
||||
meta.insert(
|
||||
"createdAt".to_string(),
|
||||
serde_json::Value::String(thread.created_at.to_rfc3339()),
|
||||
);
|
||||
if let Some(ref archived_at) = thread.archived_at {
|
||||
meta.insert(
|
||||
"archivedAt".to_string(),
|
||||
serde_json::Value::String(archived_at.to_rfc3339()),
|
||||
);
|
||||
}
|
||||
meta.insert(
|
||||
"userSetName".to_string(),
|
||||
serde_json::Value::Bool(thread.user_set_name),
|
||||
);
|
||||
if let Some(ref pid) = thread.metadata.project_id {
|
||||
meta.insert(
|
||||
"projectId".to_string(),
|
||||
serde_json::Value::String(pid.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(ref provider_id) = thread.metadata.provider_id {
|
||||
meta.insert(
|
||||
"providerId".to_string(),
|
||||
serde_json::Value::String(provider_id.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(ref model_id) = thread.metadata.model_id {
|
||||
meta.insert(
|
||||
"modelId".to_string(),
|
||||
serde_json::Value::String(model_id.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(ref persona_id) = thread.metadata.persona_id {
|
||||
meta.insert(
|
||||
"personaId".to_string(),
|
||||
serde_json::Value::String(persona_id.clone()),
|
||||
);
|
||||
}
|
||||
meta
|
||||
}
|
||||
|
||||
|
|
@ -1641,10 +1672,18 @@ impl GooseAcpAgent {
|
|||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let persona_id = args
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("personaId"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// Create the Thread — this IS the ACP session from the client's perspective.
|
||||
let thread_metadata = crate::session::ThreadMetadata {
|
||||
provider_id: requested_provider.clone(),
|
||||
project_id,
|
||||
persona_id,
|
||||
mode: Some(self.goose_mode.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
|
@ -2188,6 +2227,21 @@ impl GooseAcpAgent {
|
|||
let t_start = std::time::Instant::now();
|
||||
debug!(target: "perf", sid = %sid, "perf: prompt start");
|
||||
|
||||
// Update persona_id on the thread if the client sent one in _meta.
|
||||
let prompt_persona_id = args
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("personaId"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
if let Some(ref pid) = prompt_persona_id {
|
||||
let pid = pid.clone();
|
||||
self.update_thread_metadata(&thread_id, move |meta| {
|
||||
meta.persona_id = Some(pid);
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
let cancel_token = CancellationToken::new();
|
||||
let internal_session_id = self.internal_session_id(&thread_id).await?;
|
||||
|
||||
|
|
@ -2413,7 +2467,7 @@ impl GooseAcpAgent {
|
|||
let t_step = std::time::Instant::now();
|
||||
let model_id_owned = model_id.to_string();
|
||||
self.update_thread_metadata(thread_id, move |meta| {
|
||||
meta.model_name = Some(model_id_owned);
|
||||
meta.model_id = Some(model_id_owned);
|
||||
})
|
||||
.await?;
|
||||
debug!(target: "perf", sid = %sid, ms = t_step.elapsed().as_millis() as u64, "perf: set_model update_thread_metadata");
|
||||
|
|
@ -2635,6 +2689,7 @@ impl GooseAcpAgent {
|
|||
let provider_name_owned = provider_name.to_string();
|
||||
self.update_thread_metadata(thread_id, move |meta| {
|
||||
meta.provider_id = Some(provider_name_owned);
|
||||
meta.model_id = None;
|
||||
})
|
||||
.await?;
|
||||
debug!(target: "perf", sid = %sid, ms = t_step.elapsed().as_millis() as u64, "perf: update_provider update_thread_metadata");
|
||||
|
|
@ -2701,7 +2756,7 @@ impl GooseAcpAgent {
|
|||
.as_deref()
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_default();
|
||||
let meta = thread_session_meta(t.message_count, &t.metadata);
|
||||
let meta = thread_session_meta(&t);
|
||||
SessionInfo::new(SessionId::new(t.id), cwd)
|
||||
.title(t.name)
|
||||
.updated_at(t.updated_at.to_rfc3339())
|
||||
|
|
@ -2765,7 +2820,7 @@ impl GooseAcpAgent {
|
|||
},
|
||||
);
|
||||
|
||||
let meta = thread_session_meta(new_thread.message_count, &new_thread.metadata);
|
||||
let meta = thread_session_meta(&new_thread);
|
||||
|
||||
let mut response = ForkSessionResponse::new(SessionId::new(new_thread_id))
|
||||
.modes(mode_state)
|
||||
|
|
@ -3275,6 +3330,18 @@ impl GooseAcpAgent {
|
|||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
#[custom_method(RenameSessionRequest)]
|
||||
async fn on_rename_session(
|
||||
&self,
|
||||
req: RenameSessionRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.thread_manager
|
||||
.update_thread(&req.session_id, Some(req.title), Some(true), None)
|
||||
.await
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
#[custom_method(ArchiveSessionRequest)]
|
||||
async fn on_archive_session(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ pub struct ThreadMetadata {
|
|||
pub project_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub provider_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model_name: Option<String>,
|
||||
#[serde(default, alias = "model_name")]
|
||||
pub model_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub mode: Option<String>,
|
||||
#[serde(flatten)]
|
||||
|
|
|
|||
|
|
@ -57,12 +57,18 @@ pub async fn run_list_sessions<C: Connection>() {
|
|||
let mut response = conn.list_sessions().await.unwrap();
|
||||
for s in &mut response.sessions {
|
||||
s.updated_at = None;
|
||||
// createdAt is a dynamic timestamp — verify it exists then remove for comparison.
|
||||
if let Some(ref mut meta) = s.meta {
|
||||
assert!(meta.get("createdAt").and_then(|v| v.as_str()).is_some());
|
||||
meta.remove("createdAt");
|
||||
}
|
||||
}
|
||||
let mut expected_meta = serde_json::Map::new();
|
||||
expected_meta.insert(
|
||||
"messageCount".to_string(),
|
||||
serde_json::Value::Number(2.into()),
|
||||
);
|
||||
expected_meta.insert("userSetName".to_string(), serde_json::Value::Bool(false));
|
||||
assert_eq!(
|
||||
response,
|
||||
ListSessionsResponse::new(vec![SessionInfo::new(
|
||||
|
|
|
|||
|
|
@ -276,7 +276,6 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||
perfLog(
|
||||
`[perf:newtab] createNewTab start (project=${project?.id ?? "none"})`,
|
||||
);
|
||||
const agentId = agentStore.activeAgentId ?? undefined;
|
||||
const providerId =
|
||||
project?.preferredProvider ?? agentStore.selectedProvider ?? "goose";
|
||||
const sessionModelPreference =
|
||||
|
|
@ -312,7 +311,6 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||
const session = await sessionStore.createSession({
|
||||
title,
|
||||
projectId: project?.id,
|
||||
agentId,
|
||||
providerId: sessionModelPreference.providerId,
|
||||
workingDir,
|
||||
modelId: sessionModelPreference.modelId,
|
||||
|
|
@ -327,7 +325,6 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||
return session;
|
||||
},
|
||||
[
|
||||
agentStore.activeAgentId,
|
||||
agentStore.selectedProvider,
|
||||
chatStore,
|
||||
providerInventoryEntries,
|
||||
|
|
|
|||
|
|
@ -1,144 +0,0 @@
|
|||
import type { ModelOption } from "../types";
|
||||
|
||||
const ACP_SESSION_METADATA_STORAGE_KEY = "goose:acp-session-metadata";
|
||||
const LEGACY_SESSION_CACHE_STORAGE_KEY = "goose:chat-sessions";
|
||||
|
||||
export interface SessionMetadataOverlayRecord {
|
||||
sessionId: string;
|
||||
userSetTitle?: string | null;
|
||||
projectId?: string | null;
|
||||
providerId?: string | null;
|
||||
personaId?: string | null;
|
||||
modelId?: string | null;
|
||||
modelName?: string | null;
|
||||
archivedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
agentId?: string | null;
|
||||
lastKnownTitle?: string | null;
|
||||
lastKnownUpdatedAt?: string | null;
|
||||
lastKnownMessageCount?: number | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface LegacySessionRecord {
|
||||
id: string;
|
||||
acpSessionId?: string;
|
||||
title: string;
|
||||
projectId?: string | null;
|
||||
agentId?: string;
|
||||
providerId?: string;
|
||||
personaId?: string;
|
||||
modelId?: string;
|
||||
modelName?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
archivedAt?: string;
|
||||
messageCount: number;
|
||||
draft?: boolean;
|
||||
userSetName?: boolean;
|
||||
}
|
||||
|
||||
function parseStorageArray<T>(storageKey: string): T[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const stored = window.localStorage.getItem(storageKey);
|
||||
if (!stored) return [];
|
||||
const parsed = JSON.parse(stored);
|
||||
return Array.isArray(parsed) ? (parsed as T[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function persistStorageArray<T>(storageKey: string, records: T[]): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
if (records.length === 0) {
|
||||
window.localStorage.removeItem(storageKey);
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(records));
|
||||
} catch {
|
||||
// localStorage may be unavailable
|
||||
}
|
||||
}
|
||||
|
||||
function loadLegacySessions(): LegacySessionRecord[] {
|
||||
return parseStorageArray<LegacySessionRecord>(
|
||||
LEGACY_SESSION_CACHE_STORAGE_KEY,
|
||||
);
|
||||
}
|
||||
|
||||
function recordFromLegacySession(
|
||||
session: LegacySessionRecord,
|
||||
): SessionMetadataOverlayRecord {
|
||||
return {
|
||||
sessionId: session.acpSessionId ?? session.id,
|
||||
userSetTitle: session.userSetName ? session.title : null,
|
||||
projectId: session.projectId ?? null,
|
||||
providerId: session.providerId,
|
||||
personaId: session.personaId,
|
||||
modelId: session.modelId,
|
||||
modelName: session.modelName,
|
||||
archivedAt: session.archivedAt ?? null,
|
||||
createdAt: session.createdAt,
|
||||
agentId: session.agentId,
|
||||
lastKnownTitle: session.title,
|
||||
lastKnownUpdatedAt: session.updatedAt,
|
||||
lastKnownMessageCount: session.messageCount,
|
||||
updatedAt: session.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadSessionMetadataOverlay(): Map<
|
||||
string,
|
||||
SessionMetadataOverlayRecord
|
||||
> {
|
||||
const records = parseStorageArray<SessionMetadataOverlayRecord>(
|
||||
ACP_SESSION_METADATA_STORAGE_KEY,
|
||||
);
|
||||
const overlays = new Map(records.map((record) => [record.sessionId, record]));
|
||||
|
||||
for (const session of loadLegacySessions()) {
|
||||
if (session.draft) continue;
|
||||
const key = session.acpSessionId ?? session.id;
|
||||
if (overlays.has(key)) continue;
|
||||
overlays.set(key, recordFromLegacySession(session));
|
||||
}
|
||||
|
||||
return overlays;
|
||||
}
|
||||
|
||||
export function persistSessionMetadataOverlay(
|
||||
records: Iterable<SessionMetadataOverlayRecord>,
|
||||
): void {
|
||||
persistStorageArray(
|
||||
ACP_SESSION_METADATA_STORAGE_KEY,
|
||||
[...records].sort((a, b) => a.sessionId.localeCompare(b.sessionId)),
|
||||
);
|
||||
}
|
||||
|
||||
export function upsertSessionMetadataOverlayRecord(
|
||||
record: SessionMetadataOverlayRecord,
|
||||
): void {
|
||||
const overlays = loadSessionMetadataOverlay();
|
||||
overlays.set(record.sessionId, record);
|
||||
persistSessionMetadataOverlay(overlays.values());
|
||||
}
|
||||
|
||||
export function removeSessionMetadataOverlayRecord(sessionId: string): void {
|
||||
const overlays = loadSessionMetadataOverlay();
|
||||
if (!overlays.delete(sessionId)) return;
|
||||
persistSessionMetadataOverlay(overlays.values());
|
||||
}
|
||||
|
||||
export function modelIdsMatch(
|
||||
cached: ModelOption[] | undefined,
|
||||
next: ModelOption[],
|
||||
): boolean {
|
||||
return Boolean(
|
||||
cached &&
|
||||
cached.length === next.length &&
|
||||
cached.every((model, index) => model.id === next[index]?.id),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AcpSessionInfo } from "@/shared/api/acp";
|
||||
import { useChatSessionStore, type ChatSession } from "../chatSessionStore";
|
||||
|
||||
|
|
@ -10,8 +10,12 @@ vi.mock("@/shared/api/acp", () => ({
|
|||
acpListSessions: (...args: unknown[]) => mockAcpListSessions(...args),
|
||||
}));
|
||||
|
||||
const LEGACY_SESSION_CACHE_KEY = "goose:chat-sessions";
|
||||
const OVERLAY_CACHE_KEY = "goose:acp-session-metadata";
|
||||
vi.mock("@/shared/api/acpApi", () => ({
|
||||
archiveSession: vi.fn().mockResolvedValue(undefined),
|
||||
unarchiveSession: vi.fn().mockResolvedValue(undefined),
|
||||
renameSession: vi.fn().mockResolvedValue(undefined),
|
||||
updateSessionProject: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
function resetStore() {
|
||||
useChatSessionStore.setState({
|
||||
|
|
@ -45,16 +49,9 @@ function seedSession(overrides: Partial<ChatSession> = {}): ChatSession {
|
|||
describe("chatSessionStore", () => {
|
||||
beforeEach(() => {
|
||||
resetStore();
|
||||
window.localStorage.removeItem(LEGACY_SESSION_CACHE_KEY);
|
||||
window.localStorage.removeItem(OVERLAY_CACHE_KEY);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.localStorage.removeItem(LEGACY_SESSION_CACHE_KEY);
|
||||
window.localStorage.removeItem(OVERLAY_CACHE_KEY);
|
||||
});
|
||||
|
||||
describe("createSession", () => {
|
||||
it("creates a real ACP-backed session", async () => {
|
||||
mockAcpCreateSession.mockResolvedValue({ sessionId: "acp-1" });
|
||||
|
|
@ -96,13 +93,23 @@ describe("chatSessionStore", () => {
|
|||
sessionId: "acp-1",
|
||||
title: "ACP Session 1",
|
||||
updatedAt: "2026-04-01",
|
||||
createdAt: "2026-03-31",
|
||||
archivedAt: null,
|
||||
userSetName: false,
|
||||
messageCount: 4,
|
||||
providerId: "openai",
|
||||
modelId: "gpt-4.1",
|
||||
},
|
||||
{
|
||||
sessionId: "acp-2",
|
||||
title: null,
|
||||
updatedAt: "2026-04-02",
|
||||
createdAt: "2026-04-02",
|
||||
archivedAt: null,
|
||||
userSetName: false,
|
||||
messageCount: 7,
|
||||
providerId: null,
|
||||
modelId: null,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
@ -116,69 +123,39 @@ describe("chatSessionStore", () => {
|
|||
expect(sessions[1].id).toBe("acp-1");
|
||||
expect(sessions[1].title).toBe("ACP Session 1");
|
||||
expect(sessions[1].messageCount).toBe(4);
|
||||
expect(sessions[1].providerId).toBe("openai");
|
||||
expect(sessions[1].modelId).toBe("gpt-4.1");
|
||||
});
|
||||
|
||||
it("rehydrates cached project metadata for ACP sessions", async () => {
|
||||
window.localStorage.setItem(
|
||||
LEGACY_SESSION_CACHE_KEY,
|
||||
JSON.stringify([
|
||||
{
|
||||
id: "acp-1",
|
||||
title: "Renamed Project Chat",
|
||||
projectId: "project-123",
|
||||
providerId: "openai",
|
||||
personaId: "persona-1",
|
||||
createdAt: "2026-03-31",
|
||||
updatedAt: "2026-04-01",
|
||||
messageCount: 4,
|
||||
userSetName: true,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
it("reads all metadata fields from backend response", async () => {
|
||||
mockAcpListSessions.mockResolvedValue([
|
||||
{
|
||||
sessionId: "acp-1",
|
||||
title: null,
|
||||
title: "Renamed Chat",
|
||||
updatedAt: "2026-04-02",
|
||||
createdAt: "2026-03-31",
|
||||
archivedAt: null,
|
||||
userSetName: true,
|
||||
messageCount: 7,
|
||||
projectId: "project-123",
|
||||
providerId: "anthropic",
|
||||
personaId: "persona-1",
|
||||
modelId: "claude-sonnet-4",
|
||||
},
|
||||
]);
|
||||
|
||||
await useChatSessionStore.getState().loadSessions();
|
||||
|
||||
const session = useChatSessionStore.getState().sessions[0];
|
||||
expect(session.title).toBe("Renamed Project Chat");
|
||||
expect(session.title).toBe("Renamed Chat");
|
||||
expect(session.projectId).toBe("project-123");
|
||||
expect(session.providerId).toBe("openai");
|
||||
expect(session.providerId).toBe("anthropic");
|
||||
expect(session.personaId).toBe("persona-1");
|
||||
expect(session.createdAt).toBe("2026-03-31");
|
||||
expect(session.updatedAt).toBe("2026-04-02");
|
||||
expect(session.messageCount).toBe(7);
|
||||
expect(session.userSetName).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores legacy draft records while hydrating overlays", async () => {
|
||||
window.localStorage.setItem(
|
||||
LEGACY_SESSION_CACHE_KEY,
|
||||
JSON.stringify([
|
||||
{
|
||||
id: "cached-draft",
|
||||
title: "Cached Draft",
|
||||
draft: true,
|
||||
createdAt: "2026-04-01",
|
||||
updatedAt: "2026-04-01",
|
||||
messageCount: 0,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
mockAcpListSessions.mockResolvedValue([]);
|
||||
|
||||
await useChatSessionStore.getState().loadSessions();
|
||||
|
||||
expect(useChatSessionStore.getState().sessions).toEqual([]);
|
||||
expect(session.modelId).toBe("claude-sonnet-4");
|
||||
});
|
||||
|
||||
it("drops stale sessions that are no longer in ACP", async () => {
|
||||
|
|
@ -194,7 +171,12 @@ describe("chatSessionStore", () => {
|
|||
sessionId: "acp-1",
|
||||
title: "ACP Session",
|
||||
updatedAt: "2026-04-02",
|
||||
createdAt: "2026-04-02",
|
||||
archivedAt: null,
|
||||
userSetName: false,
|
||||
messageCount: 1,
|
||||
providerId: null,
|
||||
modelId: null,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
@ -225,39 +207,12 @@ describe("chatSessionStore", () => {
|
|||
expect(useChatSessionStore.getState().hasHydratedSessions).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to cached sessions on error", async () => {
|
||||
window.localStorage.setItem(
|
||||
LEGACY_SESSION_CACHE_KEY,
|
||||
JSON.stringify([
|
||||
{
|
||||
id: "cached-session",
|
||||
title: "Cached Session",
|
||||
projectId: "project-123",
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
updatedAt: "2026-04-01T00:00:00.000Z",
|
||||
messageCount: 8,
|
||||
},
|
||||
{
|
||||
id: "cached-draft",
|
||||
title: "Cached Draft",
|
||||
draft: true,
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
updatedAt: "2026-04-01T00:00:00.000Z",
|
||||
messageCount: 0,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
it("keeps empty sessions list on error", async () => {
|
||||
mockAcpListSessions.mockRejectedValue(new Error("Network error"));
|
||||
|
||||
await useChatSessionStore.getState().loadSessions();
|
||||
|
||||
const sessions = useChatSessionStore.getState().sessions;
|
||||
expect(sessions).toHaveLength(1);
|
||||
expect(sessions[0]).toMatchObject({
|
||||
id: "cached-session",
|
||||
projectId: "project-123",
|
||||
});
|
||||
expect(useChatSessionStore.getState().sessions).toEqual([]);
|
||||
expect(useChatSessionStore.getState().hasHydratedSessions).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,19 +7,17 @@ import {
|
|||
import type { Session } from "@/shared/types/chat";
|
||||
import { DEFAULT_CHAT_TITLE } from "@/features/chat/lib/sessionTitle";
|
||||
import {
|
||||
loadSessionMetadataOverlay,
|
||||
persistSessionMetadataOverlay,
|
||||
upsertSessionMetadataOverlayRecord,
|
||||
type SessionMetadataOverlayRecord,
|
||||
} from "@/features/chat/lib/sessionMetadataOverlay";
|
||||
import { updateSessionProject } from "@/shared/api/acpApi";
|
||||
archiveSession as acpArchiveSession,
|
||||
unarchiveSession as acpUnarchiveSession,
|
||||
renameSession as acpRenameSession,
|
||||
updateSessionProject,
|
||||
} from "@/shared/api/acpApi";
|
||||
|
||||
export interface ChatSession {
|
||||
id: string;
|
||||
acpSessionId?: string;
|
||||
title: string;
|
||||
projectId?: string | null;
|
||||
agentId?: string;
|
||||
providerId?: string;
|
||||
personaId?: string;
|
||||
modelId?: string;
|
||||
|
|
@ -66,7 +64,6 @@ interface ChatSessionStoreState {
|
|||
interface CreateSessionOpts {
|
||||
title?: string;
|
||||
projectId?: string;
|
||||
agentId?: string;
|
||||
providerId?: string;
|
||||
personaId?: string;
|
||||
workingDir?: string;
|
||||
|
|
@ -74,18 +71,10 @@ interface CreateSessionOpts {
|
|||
modelName?: string;
|
||||
}
|
||||
|
||||
interface UpdateSessionOptions {
|
||||
persistOverlay?: boolean;
|
||||
}
|
||||
|
||||
interface ChatSessionStoreActions {
|
||||
createSession: (opts?: CreateSessionOpts) => Promise<ChatSession>;
|
||||
loadSessions: () => Promise<void>;
|
||||
updateSession: (
|
||||
id: string,
|
||||
patch: Partial<ChatSession>,
|
||||
opts?: UpdateSessionOptions,
|
||||
) => void;
|
||||
updateSession: (id: string, patch: Partial<ChatSession>) => void;
|
||||
addSession: (session: ChatSession) => void;
|
||||
archiveSession: (id: string) => Promise<void>;
|
||||
unarchiveSession: (id: string) => Promise<void>;
|
||||
|
|
@ -103,101 +92,24 @@ interface ChatSessionStoreActions {
|
|||
|
||||
export type ChatSessionStore = ChatSessionStoreState & ChatSessionStoreActions;
|
||||
|
||||
function overlayKeyForSession(
|
||||
session: Pick<ChatSession, "id" | "acpSessionId">,
|
||||
) {
|
||||
return session.acpSessionId ?? session.id;
|
||||
}
|
||||
|
||||
function buildOverlayRecord(
|
||||
session: ChatSession,
|
||||
existing?: SessionMetadataOverlayRecord,
|
||||
): SessionMetadataOverlayRecord {
|
||||
return {
|
||||
sessionId: overlayKeyForSession(session),
|
||||
userSetTitle: session.userSetName ? session.title : null,
|
||||
projectId: session.projectId ?? null,
|
||||
providerId: session.providerId ?? null,
|
||||
personaId: session.personaId ?? null,
|
||||
modelId: session.modelId ?? null,
|
||||
modelName: session.modelName ?? null,
|
||||
archivedAt: session.archivedAt ?? null,
|
||||
createdAt: session.createdAt ?? existing?.createdAt ?? null,
|
||||
agentId: session.agentId ?? existing?.agentId ?? null,
|
||||
lastKnownTitle: session.title,
|
||||
lastKnownUpdatedAt: session.updatedAt,
|
||||
lastKnownMessageCount: session.messageCount,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function overlayToFallbackSession(
|
||||
overlay: SessionMetadataOverlayRecord,
|
||||
): ChatSession {
|
||||
const updatedAt =
|
||||
overlay.lastKnownUpdatedAt ?? overlay.createdAt ?? overlay.updatedAt;
|
||||
return {
|
||||
id: overlay.sessionId,
|
||||
acpSessionId: overlay.sessionId,
|
||||
title: overlay.userSetTitle ?? overlay.lastKnownTitle ?? "Untitled",
|
||||
projectId: overlay.projectId ?? undefined,
|
||||
agentId: overlay.agentId ?? undefined,
|
||||
providerId: overlay.providerId ?? undefined,
|
||||
personaId: overlay.personaId ?? undefined,
|
||||
modelId: overlay.modelId ?? undefined,
|
||||
modelName: overlay.modelName ?? undefined,
|
||||
createdAt: overlay.createdAt ?? updatedAt,
|
||||
updatedAt,
|
||||
archivedAt: overlay.archivedAt ?? undefined,
|
||||
messageCount: overlay.lastKnownMessageCount ?? 0,
|
||||
userSetName: Boolean(overlay.userSetTitle),
|
||||
};
|
||||
}
|
||||
|
||||
function mergeAcpSessionWithOverlay(
|
||||
session: AcpSessionInfo,
|
||||
overlay?: SessionMetadataOverlayRecord,
|
||||
): ChatSession {
|
||||
const updatedAt = session.updatedAt ?? overlay?.lastKnownUpdatedAt;
|
||||
function acpSessionToChatSession(session: AcpSessionInfo): ChatSession {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: session.sessionId,
|
||||
acpSessionId: session.sessionId,
|
||||
title:
|
||||
overlay?.userSetTitle ??
|
||||
session.title ??
|
||||
overlay?.lastKnownTitle ??
|
||||
"Untitled",
|
||||
title: session.title ?? "Untitled",
|
||||
projectId: session.projectId ?? undefined,
|
||||
agentId: overlay?.agentId ?? undefined,
|
||||
providerId: overlay?.providerId ?? undefined,
|
||||
personaId: overlay?.personaId ?? undefined,
|
||||
modelId: overlay?.modelId ?? undefined,
|
||||
modelName: overlay?.modelName ?? undefined,
|
||||
createdAt: overlay?.createdAt ?? updatedAt ?? new Date().toISOString(),
|
||||
updatedAt: updatedAt ?? new Date().toISOString(),
|
||||
archivedAt: overlay?.archivedAt ?? undefined,
|
||||
providerId: session.providerId ?? undefined,
|
||||
personaId: session.personaId ?? undefined,
|
||||
modelId: session.modelId ?? undefined,
|
||||
createdAt: session.createdAt ?? session.updatedAt ?? now,
|
||||
updatedAt: session.updatedAt ?? now,
|
||||
archivedAt: session.archivedAt ?? undefined,
|
||||
messageCount: session.messageCount,
|
||||
userSetName: Boolean(overlay?.userSetTitle),
|
||||
userSetName: session.userSetName,
|
||||
};
|
||||
}
|
||||
|
||||
function syncOverlaySnapshots(
|
||||
sessions: ChatSession[],
|
||||
existingOverlays = loadSessionMetadataOverlay(),
|
||||
): void {
|
||||
const overlays = new Map(existingOverlays);
|
||||
for (const session of sessions) {
|
||||
overlays.set(
|
||||
overlayKeyForSession(session),
|
||||
buildOverlayRecord(
|
||||
session,
|
||||
existingOverlays.get(overlayKeyForSession(session)),
|
||||
),
|
||||
);
|
||||
}
|
||||
persistSessionMetadataOverlay(overlays.values());
|
||||
}
|
||||
|
||||
function sortByUpdatedAtDesc(sessions: ChatSession[]): ChatSession[] {
|
||||
return [...sessions].sort(
|
||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
||||
|
|
@ -209,7 +121,6 @@ export function sessionToChatSession(session: Session): ChatSession {
|
|||
id: session.id,
|
||||
acpSessionId: session.id,
|
||||
title: session.title,
|
||||
agentId: session.agentId,
|
||||
projectId: session.projectId,
|
||||
providerId: session.providerId,
|
||||
personaId: session.personaId,
|
||||
|
|
@ -247,7 +158,6 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
|||
acpSessionId: sessionId,
|
||||
title: opts.title ?? DEFAULT_CHAT_TITLE,
|
||||
projectId: opts.projectId,
|
||||
agentId: opts.agentId,
|
||||
providerId,
|
||||
personaId: opts.personaId,
|
||||
modelId: opts.modelId,
|
||||
|
|
@ -257,48 +167,32 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
|||
messageCount: 0,
|
||||
};
|
||||
set((state) => ({ sessions: [chatSession, ...state.sessions] }));
|
||||
const existing = loadSessionMetadataOverlay().get(
|
||||
overlayKeyForSession(chatSession),
|
||||
);
|
||||
upsertSessionMetadataOverlayRecord(
|
||||
buildOverlayRecord(chatSession, existing),
|
||||
);
|
||||
return chatSession;
|
||||
},
|
||||
|
||||
loadSessions: async () => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const overlays = loadSessionMetadataOverlay();
|
||||
const acpSessions = await acpListSessions();
|
||||
const mergedAcpSessions = sortByUpdatedAtDesc(
|
||||
acpSessions.map((session) =>
|
||||
mergeAcpSessionWithOverlay(session, overlays.get(session.sessionId)),
|
||||
),
|
||||
const sessions = sortByUpdatedAtDesc(
|
||||
acpSessions.map(acpSessionToChatSession),
|
||||
);
|
||||
const merged = mergedAcpSessions;
|
||||
const activeSessionId = get().activeSessionId;
|
||||
const activeSessionStillExists =
|
||||
activeSessionId == null ||
|
||||
merged.some((session) => session.id === activeSessionId);
|
||||
sessions.some((session) => session.id === activeSessionId);
|
||||
set({
|
||||
sessions: merged,
|
||||
sessions,
|
||||
activeSessionId: activeSessionStillExists ? activeSessionId : null,
|
||||
});
|
||||
syncOverlaySnapshots(mergedAcpSessions, overlays);
|
||||
} catch (error) {
|
||||
console.error("Failed to load sessions from ACP:", error);
|
||||
const overlays = loadSessionMetadataOverlay();
|
||||
const fallbackSessions = sortByUpdatedAtDesc(
|
||||
[...overlays.values()].map(overlayToFallbackSession),
|
||||
);
|
||||
set({ sessions: fallbackSessions });
|
||||
} finally {
|
||||
set({ isLoading: false, hasHydratedSessions: true });
|
||||
}
|
||||
},
|
||||
|
||||
updateSession: (id, patch, opts) => {
|
||||
updateSession: (id, patch) => {
|
||||
set((state) => ({
|
||||
sessions: state.sessions.map((session) =>
|
||||
session.id === id
|
||||
|
|
@ -312,17 +206,22 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
|||
}));
|
||||
|
||||
const updatedSession = get().sessions.find((session) => session.id === id);
|
||||
const acpSessionId = updatedSession?.acpSessionId;
|
||||
|
||||
if (updatedSession && opts?.persistOverlay !== false) {
|
||||
const key = overlayKeyForSession(updatedSession);
|
||||
const existing = loadSessionMetadataOverlay().get(key);
|
||||
upsertSessionMetadataOverlayRecord(
|
||||
buildOverlayRecord(updatedSession, existing),
|
||||
// Persist title rename to backend
|
||||
if (
|
||||
"title" in patch &&
|
||||
"userSetName" in patch &&
|
||||
patch.userSetName &&
|
||||
acpSessionId &&
|
||||
patch.title
|
||||
) {
|
||||
acpRenameSession(acpSessionId, patch.title).catch((err: unknown) =>
|
||||
console.error("Failed to rename session in backend:", err),
|
||||
);
|
||||
}
|
||||
|
||||
// Persist projectId change to ACP backend
|
||||
const acpSessionId = updatedSession?.acpSessionId;
|
||||
// Persist projectId change to backend
|
||||
if ("projectId" in patch && acpSessionId) {
|
||||
updateSessionProject(acpSessionId, patch.projectId ?? null).catch(
|
||||
(err: unknown) =>
|
||||
|
|
@ -347,12 +246,6 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
|||
}
|
||||
return { sessions: [normalizedSession, ...state.sessions] };
|
||||
});
|
||||
const existing = loadSessionMetadataOverlay().get(
|
||||
overlayKeyForSession(normalizedSession),
|
||||
);
|
||||
upsertSessionMetadataOverlayRecord(
|
||||
buildOverlayRecord(normalizedSession, existing),
|
||||
);
|
||||
},
|
||||
|
||||
archiveSession: async (id) => {
|
||||
|
|
@ -366,11 +259,10 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
|||
state.activeSessionId === id ? null : state.activeSessionId,
|
||||
}));
|
||||
const session = get().sessions.find((candidate) => candidate.id === id);
|
||||
if (session) {
|
||||
const existing = loadSessionMetadataOverlay().get(
|
||||
overlayKeyForSession(session),
|
||||
if (session?.acpSessionId) {
|
||||
acpArchiveSession(session.acpSessionId).catch((err: unknown) =>
|
||||
console.error("Failed to archive session in backend:", err),
|
||||
);
|
||||
upsertSessionMetadataOverlayRecord(buildOverlayRecord(session, existing));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -381,11 +273,10 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
|||
),
|
||||
}));
|
||||
const session = get().sessions.find((candidate) => candidate.id === id);
|
||||
if (session) {
|
||||
const existing = loadSessionMetadataOverlay().get(
|
||||
overlayKeyForSession(session),
|
||||
if (session?.acpSessionId) {
|
||||
acpUnarchiveSession(session.acpSessionId).catch((err: unknown) =>
|
||||
console.error("Failed to unarchive session in backend:", err),
|
||||
);
|
||||
upsertSessionMetadataOverlayRecord(buildOverlayRecord(session, existing));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -433,15 +324,6 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
|||
: session,
|
||||
),
|
||||
}));
|
||||
const session = get().sessions.find(
|
||||
(candidate) => candidate.id === sessionId,
|
||||
);
|
||||
if (session) {
|
||||
const existing = loadSessionMetadataOverlay().get(
|
||||
overlayKeyForSession(session),
|
||||
);
|
||||
upsertSessionMetadataOverlayRecord(buildOverlayRecord(session, existing));
|
||||
}
|
||||
},
|
||||
|
||||
getSession: (id) => get().sessions.find((session) => session.id === id),
|
||||
|
|
|
|||
|
|
@ -95,7 +95,13 @@ export async function acpSendMessage(
|
|||
`[perf:send] ${sid} acpSendMessage → prompt(len=${prompt.length}, imgs=${images?.length ?? 0})`,
|
||||
);
|
||||
const tPrompt = performance.now();
|
||||
await directAcp.prompt(gooseSessionId, content);
|
||||
const meta: Record<string, unknown> = {};
|
||||
if (personaId) meta.personaId = personaId;
|
||||
await directAcp.prompt(
|
||||
gooseSessionId,
|
||||
content,
|
||||
Object.keys(meta).length > 0 ? meta : undefined,
|
||||
);
|
||||
const tDone = performance.now();
|
||||
perfLog(
|
||||
`[perf:send] ${sid} prompt() resolved in ${(tDone - tPrompt).toFixed(1)}ms (total acpSendMessage ${(tDone - tStart).toFixed(1)}ms)`,
|
||||
|
|
|
|||
|
|
@ -17,8 +17,14 @@ export interface AcpSessionInfo {
|
|||
sessionId: string;
|
||||
title: string | null;
|
||||
updatedAt: string | null;
|
||||
createdAt: string | null;
|
||||
archivedAt: string | null;
|
||||
userSetName: boolean;
|
||||
messageCount: number;
|
||||
projectId?: string | null;
|
||||
providerId: string | null;
|
||||
modelId: string | null;
|
||||
personaId: string | null;
|
||||
}
|
||||
|
||||
const DEPRECATED_PROVIDER_IDS = new Set(["claude-code", "codex", "gemini-cli"]);
|
||||
|
|
@ -50,8 +56,14 @@ export async function listSessions(): Promise<AcpSessionInfo[]> {
|
|||
sessionId: info.sessionId,
|
||||
title: info.title ?? null,
|
||||
updatedAt: info.updatedAt ?? null,
|
||||
createdAt: (info._meta?.createdAt as string) ?? null,
|
||||
archivedAt: (info._meta?.archivedAt as string) ?? null,
|
||||
userSetName: info._meta?.userSetName === true,
|
||||
messageCount: (info._meta?.messageCount as number) ?? 0,
|
||||
projectId: (info._meta?.projectId as string) ?? null,
|
||||
providerId: (info._meta?.providerId as string) ?? null,
|
||||
modelId: (info._meta?.modelId as string) ?? null,
|
||||
personaId: (info._meta?.personaId as string) ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +90,14 @@ export async function forkSession(sessionId: string): Promise<AcpSessionInfo> {
|
|||
sessionId: response.sessionId,
|
||||
title: (response._meta?.title as string) ?? null,
|
||||
updatedAt: null,
|
||||
createdAt: (response._meta?.createdAt as string) ?? null,
|
||||
archivedAt: (response._meta?.archivedAt as string) ?? null,
|
||||
userSetName: response._meta?.userSetName === true,
|
||||
messageCount: (response._meta?.messageCount as number) ?? 0,
|
||||
projectId: (response._meta?.projectId as string) ?? null,
|
||||
providerId: (response._meta?.providerId as string) ?? null,
|
||||
modelId: (response._meta?.modelId as string) ?? null,
|
||||
personaId: (response._meta?.personaId as string) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -137,6 +156,24 @@ export async function updateSessionProject(
|
|||
});
|
||||
}
|
||||
|
||||
export async function archiveSession(sessionId: string): Promise<void> {
|
||||
const client = await getClient();
|
||||
await client.extMethod("_goose/session/archive", { sessionId });
|
||||
}
|
||||
|
||||
export async function unarchiveSession(sessionId: string): Promise<void> {
|
||||
const client = await getClient();
|
||||
await client.extMethod("_goose/session/unarchive", { sessionId });
|
||||
}
|
||||
|
||||
export async function renameSession(
|
||||
sessionId: string,
|
||||
title: string,
|
||||
): Promise<void> {
|
||||
const client = await getClient();
|
||||
await client.extMethod("_goose/session/rename", { sessionId, title });
|
||||
}
|
||||
|
||||
export async function cancelSession(sessionId: string): Promise<void> {
|
||||
const client = await getClient();
|
||||
await client.cancel({ sessionId });
|
||||
|
|
@ -146,6 +183,7 @@ export async function newSession(
|
|||
workingDir: string,
|
||||
providerId?: string,
|
||||
projectId?: string,
|
||||
personaId?: string,
|
||||
): Promise<NewSessionResponse> {
|
||||
const tClient = performance.now();
|
||||
const client = await getClient();
|
||||
|
|
@ -159,6 +197,7 @@ export async function newSession(
|
|||
const meta: Record<string, string> = {};
|
||||
if (providerId) meta.provider = providerId;
|
||||
if (projectId) meta.projectId = projectId;
|
||||
if (personaId) meta.personaId = personaId;
|
||||
if (Object.keys(meta).length > 0) request.meta = meta;
|
||||
|
||||
const tCall = performance.now();
|
||||
|
|
@ -192,7 +231,8 @@ export async function loadSession(
|
|||
export async function prompt(
|
||||
sessionId: string,
|
||||
content: ContentBlock[],
|
||||
meta?: Record<string, unknown>,
|
||||
): Promise<PromptResponse> {
|
||||
const client = await getClient();
|
||||
return client.prompt({ sessionId, prompt: content });
|
||||
return client.prompt({ sessionId, prompt: content, _meta: meta });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -415,11 +415,7 @@ function handleShared(sessionId: string, update: SessionUpdate): void {
|
|||
if (session && !session.userSetName) {
|
||||
useChatSessionStore
|
||||
.getState()
|
||||
.updateSession(
|
||||
sessionId,
|
||||
{ title: info.title as string },
|
||||
{ persistOverlay: false },
|
||||
);
|
||||
.updateSession(sessionId, { title: info.title as string });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -456,11 +452,10 @@ function handleShared(sessionId: string, update: SessionUpdate): void {
|
|||
currentModelId;
|
||||
|
||||
const sessionStore = useChatSessionStore.getState();
|
||||
sessionStore.updateSession(
|
||||
sessionId,
|
||||
{ modelId: currentModelId, modelName: currentModelName },
|
||||
{ persistOverlay: false },
|
||||
);
|
||||
sessionStore.updateSession(sessionId, {
|
||||
modelId: currentModelId,
|
||||
modelName: currentModelName,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -102,7 +102,12 @@ export async function prepareSession(
|
|||
|
||||
if (!gooseSessionId) {
|
||||
const tNew = performance.now();
|
||||
const response = await acpApi.newSession(workingDir, providerId, projectId);
|
||||
const response = await acpApi.newSession(
|
||||
workingDir,
|
||||
providerId,
|
||||
projectId,
|
||||
personaId,
|
||||
);
|
||||
gooseSessionId = response.sessionId;
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} tracker newSession done in ${(performance.now() - tNew).toFixed(1)}ms (goose_sid=${gooseSessionId.slice(0, 8)})`,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ export const INITIAL_SESSION_CHAT_RUNTIME: SessionChatRuntime = {
|
|||
export interface Session {
|
||||
id: string;
|
||||
title: string;
|
||||
agentId?: string;
|
||||
projectId?: string | null;
|
||||
providerId?: string;
|
||||
personaId?: string;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import type {
|
|||
RemoveConfigRequest,
|
||||
RemoveExtensionRequest,
|
||||
RemoveSecretRequest,
|
||||
RenameSessionRequest,
|
||||
ToggleConfigExtensionRequest,
|
||||
UnarchiveSessionRequest,
|
||||
UpdateSessionProjectRequest,
|
||||
|
|
@ -222,6 +223,10 @@ export class GooseExtClient {
|
|||
await this.conn.extMethod("_goose/session/update_project", params);
|
||||
}
|
||||
|
||||
async GooseSessionRename(params: RenameSessionRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/session/rename", params);
|
||||
}
|
||||
|
||||
async GooseSessionArchive(params: ArchiveSessionRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/session/archive", params);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type { AddConfigExtensionRequest, AddExtensionRequest, ArchiveSessionRequest, CheckSecretRequest, CheckSecretResponse, CreateSourceRequest, CreateSourceResponse, DeleteSessionRequest, DeleteSourceRequest, DictationConfigRequest, DictationConfigResponse, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest, DictationModelDeleteRequest, DictationModelDownloadProgressRequest, DictationModelDownloadProgressResponse, DictationModelDownloadRequest, DictationModelOption, DictationModelSelectRequest, DictationModelsListRequest, DictationModelsListResponse, DictationProviderStatusEntry, DictationTranscribeRequest, DictationTranscribeResponse, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExportSourceRequest, ExportSourceResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ImportSourcesRequest, ImportSourcesResponse, ListProvidersRequest, ListProvidersResponse, ListSourcesRequest, ListSourcesResponse, ProviderConfigKey, ProviderInventoryEntryDto, ProviderInventoryModelDto, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RefreshProviderInventoryRequest, RefreshProviderInventoryResponse, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, SourceEntry, SourceType, ToggleConfigExtensionRequest, UnarchiveSessionRequest, UpdateSessionProjectRequest, UpdateSourceRequest, UpdateSourceResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js';
|
||||
export type { AddConfigExtensionRequest, AddExtensionRequest, ArchiveSessionRequest, CheckSecretRequest, CheckSecretResponse, CreateSourceRequest, CreateSourceResponse, DeleteSessionRequest, DeleteSourceRequest, DictationConfigRequest, DictationConfigResponse, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest, DictationModelDeleteRequest, DictationModelDownloadProgressRequest, DictationModelDownloadProgressResponse, DictationModelDownloadRequest, DictationModelOption, DictationModelSelectRequest, DictationModelsListRequest, DictationModelsListResponse, DictationProviderStatusEntry, DictationTranscribeRequest, DictationTranscribeResponse, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExportSourceRequest, ExportSourceResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ImportSourcesRequest, ImportSourcesResponse, ListProvidersRequest, ListProvidersResponse, ListSourcesRequest, ListSourcesResponse, ProviderConfigKey, ProviderInventoryEntryDto, ProviderInventoryModelDto, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RefreshProviderInventoryRequest, RefreshProviderInventoryResponse, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, RenameSessionRequest, SourceEntry, SourceType, ToggleConfigExtensionRequest, UnarchiveSessionRequest, UpdateSessionProjectRequest, UpdateSourceRequest, UpdateSourceResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js';
|
||||
|
||||
export const GOOSE_EXT_METHODS = [
|
||||
{
|
||||
|
|
@ -113,6 +113,11 @@ export const GOOSE_EXT_METHODS = [
|
|||
requestType: "UpdateSessionProjectRequest",
|
||||
responseType: "EmptyResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/session/rename",
|
||||
requestType: "RenameSessionRequest",
|
||||
responseType: "EmptyResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/session/archive",
|
||||
requestType: "ArchiveSessionRequest",
|
||||
|
|
|
|||
|
|
@ -388,6 +388,14 @@ export type UpdateSessionProjectRequest = {
|
|||
projectId?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Rename a session.
|
||||
*/
|
||||
export type RenameSessionRequest = {
|
||||
sessionId: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Archive a session (soft delete).
|
||||
*/
|
||||
|
|
@ -659,7 +667,7 @@ export type DictationModelSelectRequest = {
|
|||
export type ExtRequest = {
|
||||
id: string;
|
||||
method: string;
|
||||
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | AddConfigExtensionRequest | RemoveConfigExtensionRequest | ToggleConfigExtensionRequest | GetSessionExtensionsRequest | ListProvidersRequest | RefreshProviderInventoryRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | UpdateSessionProjectRequest | ArchiveSessionRequest | UnarchiveSessionRequest | CreateSourceRequest | ListSourcesRequest | UpdateSourceRequest | DeleteSourceRequest | ExportSourceRequest | ImportSourcesRequest | DictationTranscribeRequest | DictationConfigRequest | DictationModelsListRequest | DictationModelDownloadRequest | DictationModelDownloadProgressRequest | DictationModelCancelRequest | DictationModelDeleteRequest | DictationModelSelectRequest | {
|
||||
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | AddConfigExtensionRequest | RemoveConfigExtensionRequest | ToggleConfigExtensionRequest | GetSessionExtensionsRequest | ListProvidersRequest | RefreshProviderInventoryRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | UpdateSessionProjectRequest | RenameSessionRequest | ArchiveSessionRequest | UnarchiveSessionRequest | CreateSourceRequest | ListSourcesRequest | UpdateSourceRequest | DeleteSourceRequest | ExportSourceRequest | ImportSourcesRequest | DictationTranscribeRequest | DictationConfigRequest | DictationModelsListRequest | DictationModelDownloadRequest | DictationModelDownloadProgressRequest | DictationModelCancelRequest | DictationModelDeleteRequest | DictationModelSelectRequest | {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -328,6 +328,14 @@ export const zUpdateSessionProjectRequest = z.object({
|
|||
]).optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* Rename a session.
|
||||
*/
|
||||
export const zRenameSessionRequest = z.object({
|
||||
sessionId: z.string(),
|
||||
title: z.string()
|
||||
});
|
||||
|
||||
/**
|
||||
* Archive a session (soft delete).
|
||||
*/
|
||||
|
|
@ -629,6 +637,7 @@ export const zExtRequest = z.object({
|
|||
zExportSessionRequest,
|
||||
zImportSessionRequest,
|
||||
zUpdateSessionProjectRequest,
|
||||
zRenameSessionRequest,
|
||||
zArchiveSessionRequest,
|
||||
zUnarchiveSessionRequest,
|
||||
zCreateSourceRequest,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue