diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 521dcfe725..230558c8ed 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -160,7 +160,7 @@ export namespace Session { }, } log.info("created", result) - await Storage.write(["session", Instance.project.id, result.id], result) + await Storage.write(["session", "global", result.id], result) const cfg = await Config.get() if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto")) share(result.id) @@ -179,9 +179,24 @@ export namespace Session { } export const get = fn(Identifier.schema("session"), async (id) => { - const read = await Storage.read(["session", Instance.project.id, id]) - return read as Info - }) + try { + // 1. Try new global path + return await Storage.read(["session", "global", id]); + } catch (e) { + try { + // 2. Try old project-specific path + const session = await Storage.read(["session", Instance.project.id, id]); + // 3. Migrate if found + await Storage.write(["session", "global", id], session); + await Storage.remove(["session", Instance.project.id, id]); + log.info("Lazily migrated session to global store", { sessionID: id }); + return session; + } catch (e2) { + // 4. Not found anywhere, re-throw original error + throw e; + } + } + }); export const getShare = fn(Identifier.schema("session"), async (id) => { return Storage.read(["share", id]) @@ -223,15 +238,16 @@ export namespace Session { }) export async function update(id: string, editor: (session: Info) => void) { - const project = Instance.project - const result = await Storage.update(["session", project.id, id], (draft) => { - editor(draft) - draft.time.updated = Date.now() - }) + // Ensure session is migrated by calling get first, which handles the move. + await get(id); + const result = await Storage.update(["session", "global", id], (draft) => { + editor(draft); + draft.time.updated = Date.now(); + }); Bus.publish(Event.Updated, { info: result, - }) - return result + }); + return result; } export const messages = fn(Identifier.schema("session"), async (sessionID) => { @@ -272,8 +288,31 @@ export namespace Session { export async function* list() { const project = Instance.project - for (const item of await Storage.list(["session", project.id])) { - yield Storage.read(item) + const seen = new Set() + + // 1. List from the new global path + try { + for (const item of await Storage.list(["session", "global"])) { + const session = await Storage.read(item) + if (session.directory === Instance.directory) { + seen.add(session.id) + yield session + } + } + } catch (e) { + /* global dir might not exist yet */ + } + + // 2. List from the old project-specific path (for backwards compatibility) + try { + for (const item of await Storage.list(["session", project.id])) { + const session = await Storage.read(item) + if (!seen.has(session.id) && session.directory === Instance.directory) { + yield session + } + } + } catch (e) { + /* project dir might not exist */ } } @@ -302,7 +341,17 @@ export namespace Session { } await Storage.remove(msg) } - await Storage.remove(["session", project.id, sessionID]) + // Try removing from both new and old paths to be safe. + try { + await Storage.remove(["session", "global", sessionID]); + } catch (e) { + // Might not exist in global, try old path + try { + await Storage.remove(["session", project.id, sessionID]); + } catch (e2) { + log.warn("Could not remove session from any known location", { sessionID }); + } + } Bus.publish(Event.Deleted, { info: session, }) diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 4a891f2827..9ac9eaca4f 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -957,6 +957,32 @@ func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, er return messages, nil } +func (a *App) LoadLastSession() tea.Cmd { + return func() tea.Msg { + sessions, err := a.ListSessions(context.Background()) + if err != nil { + slog.Error("Failed to list sessions for initial session", "error", err) + return toast.NewErrorToast("Failed to load initial session")() + } + + if len(sessions) > 0 { + var lastSession opencode.Session + for _, session := range sessions { + if session.ParentID == "" { + if lastSession.ID == "" || session.Time.Updated > lastSession.Time.Updated { + lastSession = session + } + } + } + if lastSession.ID != "" { + return SessionSelectedMsg(&lastSession) + } + } + + return nil + } +} + func (a *App) ListProviders(ctx context.Context) ([]opencode.Provider, error) { response, err := a.Client.App.Providers(ctx, opencode.AppProvidersParams{}) if err != nil { diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 50b503c66b..3b27ac9bc8 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -87,6 +87,7 @@ func (a Model) Init() tea.Cmd { cmds = append(cmds, tea.RequestBackgroundColor) } cmds = append(cmds, a.app.InitializeProvider()) + cmds = append(cmds, a.app.LoadLastSession()) cmds = append(cmds, a.editor.Init()) cmds = append(cmds, a.messages.Init()) cmds = append(cmds, a.status.Init())