Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions packages/tui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import { PromptStashProvider } from "./component/prompt/stash"
import { DialogAlert } from "./ui/dialog-alert"
import { DialogConfirm } from "./ui/dialog-confirm"
import { ToastProvider, useToast } from "./ui/toast"
import { isDefaultTitle } from "./util/session"
import { isDefaultTitle, selectContinueSession, selectEmptySession } from "./util/session"
import { KVProvider, useKV } from "./context/kv"
import * as Model from "./util/model"
import { ArgsProvider, useArgs, type Args } from "./context/args"
Expand Down Expand Up @@ -525,11 +525,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi

async function findEmptySession() {
const list = await sdk.client.session.list({ start: Date.now() - 30 * 24 * 60 * 60 * 1000 })
const match = (list.data ?? [])
.filter((x) => !x.parentID && !x.time.archived && isDefaultTitle(x.title))
.filter((x) => !sdk.directory || x.directory === sdk.directory)
.toSorted((a, b) => b.time.updated - a.time.updated)
.at(0)
const match = selectEmptySession(list.data ?? [], sdk.directory)
if (!match) return
const messages = await sdk.client.session.messages({ sessionID: match.id, limit: 1 })
if (messages.data?.length !== 0) return
Expand Down Expand Up @@ -557,10 +553,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
if (continued || sync.status === "loading" || !args.continue) return
// Skip default-titled sessions: those are the empty ones auto-created at
// startup, and -c should land on the last real conversation.
const sorted = sync.data.session
.toSorted((a, b) => b.time.updated - a.time.updated)
.filter((x) => x.parentID === undefined)
const match = (sorted.find((x) => !isDefaultTitle(x.title)) ?? sorted[0])?.id
const match = selectContinueSession(sync.data.session)?.id
if (match) {
continued = true
if (args.fork) {
Expand Down
26 changes: 26 additions & 0 deletions packages/tui/src/util/session.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
export function isDefaultTitle(title: string) {
return /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(title)
}

type SessionSummary = {
title: string
directory?: string
parentID?: string
time: { updated: number; archived?: number }
}

// Fork: the empty session that plain startup reuses — the newest parentless,
// unarchived, default-titled session in the current directory. Callers still
// confirm it has zero messages before adopting it.
export function selectEmptySession<T extends SessionSummary>(sessions: readonly T[], directory?: string) {
return sessions
.filter((x) => !x.parentID && !x.time.archived && isDefaultTitle(x.title))
.filter((x) => !directory || x.directory === directory)
.toSorted((a, b) => b.time.updated - a.time.updated)
.at(0)
}

// Fork: the session `-c` lands on — the newest parentless conversation,
// skipping default-titled ones (the empty sessions auto-created at startup) so
// it resumes the last real conversation, falling back to the newest overall.
export function selectContinueSession<T extends SessionSummary>(sessions: readonly T[]) {
const sorted = sessions.toSorted((a, b) => b.time.updated - a.time.updated).filter((x) => x.parentID === undefined)
return sorted.find((x) => !isDefaultTitle(x.title)) ?? sorted.at(0)
}
85 changes: 84 additions & 1 deletion packages/tui/test/util/session.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { isDefaultTitle } from "../../src/util/session"
import { isDefaultTitle, selectContinueSession, selectEmptySession } from "../../src/util/session"

describe("util.session", () => {
test("recognizes generated parent and child titles", () => {
Expand All @@ -8,3 +8,86 @@ describe("util.session", () => {
expect(isDefaultTitle("New session - custom")).toBeFalse()
})
})

function session(overrides: {
id: string
title?: string
directory?: string
parentID?: string
archived?: number
updated?: number
}) {
return {
id: overrides.id,
title: overrides.title ?? "New session - 2026-06-06T12:34:56.789Z",
directory: overrides.directory,
parentID: overrides.parentID,
time: { updated: overrides.updated ?? 0, archived: overrides.archived },
}
}

describe("selectEmptySession", () => {
test("returns the newest reusable empty session in the directory", () => {
const match = selectEmptySession(
[
session({ id: "old", directory: "/work", updated: 1 }),
session({ id: "new", directory: "/work", updated: 5 }),
],
"/work",
)
expect(match?.id).toBe("new")
})

test("ignores archived, parented, titled, and other-directory sessions", () => {
const match = selectEmptySession(
[
session({ id: "archived", directory: "/work", archived: 123, updated: 9 }),
session({ id: "child", directory: "/work", parentID: "root", updated: 9 }),
session({ id: "titled", directory: "/work", title: "Fix the bug", updated: 9 }),
session({ id: "elsewhere", directory: "/other", updated: 9 }),
session({ id: "reusable", directory: "/work", updated: 2 }),
],
"/work",
)
expect(match?.id).toBe("reusable")
})

test("matches any directory when none is provided", () => {
const match = selectEmptySession([session({ id: "any", directory: "/somewhere", updated: 3 })])
expect(match?.id).toBe("any")
})

test("returns undefined when nothing is reusable", () => {
expect(selectEmptySession([session({ id: "titled", title: "Real work" })], "/work")).toBeUndefined()
})
})

describe("selectContinueSession", () => {
test("prefers the newest non-default-titled conversation", () => {
const match = selectContinueSession([
session({ id: "empty", updated: 10 }),
session({ id: "real", title: "Ship the feature", updated: 4 }),
])
expect(match?.id).toBe("real")
})

test("falls back to the newest session when all are default-titled", () => {
const match = selectContinueSession([
session({ id: "older", updated: 1 }),
session({ id: "newer", updated: 8 }),
])
expect(match?.id).toBe("newer")
})

test("skips child sessions", () => {
const match = selectContinueSession([
session({ id: "child", title: "Ship the feature", parentID: "root", updated: 10 }),
session({ id: "root", title: "Earlier work", updated: 3 }),
])
expect(match?.id).toBe("root")
})

test("returns undefined for an empty list", () => {
expect(selectContinueSession([])).toBeUndefined()
})
})
Loading