feat(web): render durable session titles

This commit is contained in:
Tianyi Cui
2026-07-22 23:43:53 +08:00
parent 690c53dc03
commit a9ea193e31
39 changed files with 481 additions and 57 deletions

View File

@@ -54,7 +54,7 @@ export function ConversationRoot({
disabled={last}
onClick={() => { actions.open(s.id) }}
>
{s.title}
{s.displayTitle}
</button>
</span>
)

View File

@@ -50,7 +50,7 @@ async function bench() {
const listStore = createSnapshotStore<SessionListState>({
ids: [ROOT],
byId: { [ROOT]: { id: ROOT, title: 'R', cwd: '/proj', running: false, updatedAt: 1 } },
byId: { [ROOT]: { id: ROOT, title: 'R', displayTitle: 'R', cwd: '/proj', running: false, updatedAt: 1 } },
})
const snap = snapshotBase()
const sessionFake = {
@@ -208,7 +208,7 @@ describe('conversation slot inject surface', () => {
// Ancestry and draft/active-view hooks execute inside a component tree.
const HookProbe = () => {
const injected2 = b.entryOf('conversation').options.inject(b.binding) as {
useAncestry: () => readonly { title: string }[]
useAncestry: () => readonly { displayTitle: string }[]
useActiveView: () => string | undefined
composer: { useDraft: () => string }
}

View File

@@ -26,8 +26,8 @@ async function bench() {
const listStore = createSnapshotStore<SessionListState>({
ids: [ROOT, CHILD],
byId: {
[ROOT]: { id: ROOT, title: 'R', running: false, updatedAt: 1 },
[CHILD]: { id: CHILD, title: 'C', parentId: ROOT, running: false, updatedAt: 2 },
[ROOT]: { id: ROOT, title: 'R', displayTitle: 'R', running: false, updatedAt: 1 },
[CHILD]: { id: CHILD, title: 'C', displayTitle: 'C', parentId: ROOT, running: false, updatedAt: 2 },
},
})
const sessionsFake = {

View File

@@ -155,8 +155,8 @@ describe('bash toolview samples', () => {
getSnapshot: () => ({
ids: [root, child],
byId: {
[root]: { id: root, title: 'r', running: false, updatedAt: 0 },
[child]: { id: child, title: 'c', parentId: root, running: false, updatedAt: 0 },
[root]: { id: root, title: 'r', displayTitle: 'r', running: false, updatedAt: 0 },
[child]: { id: child, title: 'c', displayTitle: 'c', parentId: root, running: false, updatedAt: 0 },
},
}),
})

View File

@@ -49,9 +49,9 @@ describe('apply need() and cwd cache', () => {
const listStore = createSnapshotStore<SessionListState>({
ids: [SID, 'x2' as SessionId, 'x3' as SessionId],
byId: {
[SID]: { id: SID, title: 'a', cwd: '/proj', running: false, updatedAt: 1 },
['x2' as SessionId]: { id: 'x2' as SessionId, title: 'b', cwd: '', running: false, updatedAt: 1 },
['x3' as SessionId]: { id: 'x3' as SessionId, title: 'c', running: false, updatedAt: 1 },
[SID]: { id: SID, title: 'a', displayTitle: 'a', cwd: '/proj', running: false, updatedAt: 1 },
['x2' as SessionId]: { id: 'x2' as SessionId, title: 'b', displayTitle: 'b', cwd: '', running: false, updatedAt: 1 },
['x3' as SessionId]: { id: 'x3' as SessionId, title: 'c', displayTitle: 'c', running: false, updatedAt: 1 },
},
})
ctx.provide('sessions', { list: listStore, manager: { get: vi.fn() }, ancestry: () => [], scope: () => undefined, create: vi.fn() })

View File

@@ -49,13 +49,14 @@ function feed(b: Bench, rows: { id: string; cwd?: string; running?: boolean }[])
}
describe('selection survives list refreshes (M1a)', () => {
it('create → select → title-upgrading refresh keeps scope, binding, store and value', async () => {
it('create → select → display-title-upgrading refresh keeps scope, binding, store and value', async () => {
const b = bench()
// First-send shape: client-side create inserts the row without cwd (title = bare id).
b.api.onCreate = () => Promise.resolve(ok({ sessionId: sid('s1') }))
const id = await b.sessions.create({})
await flush()
expect(b.sessions.list.getSnapshot().byId[id]?.title).toBe('s1')
expect(b.sessions.list.getSnapshot().byId[id]).toMatchObject({ displayTitle: 's1' })
expect(b.sessions.list.getSnapshot().byId[id]?.title).toBeUndefined()
const binding = b.sessions.binding(id)
expect(binding).toBeDefined()
@@ -63,11 +64,12 @@ describe('selection survives list refreshes (M1a)', () => {
const store = (scoped.get('conversation') as ConversationService).selection
store.set({ turnSeq: 3, callId: 'c1' })
// The late list refresh lands (host knows the cwd → formal title).
// The late list refresh lands (host knows the cwd → better fallback label).
feed(b, [{ id: 's1', cwd: '/w/proj-a' }])
await b.sessions.manager.refreshList()
await flush()
expect(b.sessions.list.getSnapshot().byId[id]?.title).toBe('proj-a')
expect(b.sessions.list.getSnapshot().byId[id]).toMatchObject({ displayTitle: 'proj-a' })
expect(b.sessions.list.getSnapshot().byId[id]?.title).toBeUndefined()
// Scope, binding and the selection account must all be identity-stable.
expect(b.sessions.scope(id)).toBe(scoped)
@@ -87,7 +89,7 @@ describe('selection survives list refreshes (M1a)', () => {
const store = (scoped.get('conversation') as ConversationService).selection
store.set({ turnSeq: 1, callId: 'c9' })
// Reconnect generation: title upgrade arrives with the re-pull.
// Reconnect generation: display-title fallback upgrade arrives with the re-pull.
feed(b, [{ id: 's1', cwd: '/w/proj-a', running: true }])
b.sessions.manager.handleConnected()
await flush()

View File

@@ -33,7 +33,7 @@ function sessionSource(over?: Partial<ConversationSnapshot>) {
}
const summary = (id: string, title: string): SessionSummary =>
({ id: id as SessionId, title, running: false, updatedAt: 1 })
({ id: id as SessionId, title: `durable ${title}`, displayTitle: title, running: false, updatedAt: 1 })
describe('ConversationRoot branches', () => {
const chatEntry: ViewEntry = {

View File

@@ -76,8 +76,8 @@ describe('ConversationRoot', () => {
const send = vi.fn()
const stop = vi.fn()
const ancestry: SessionSummary[] = [
{ id: sid('root'), title: 'proj', running: false, updatedAt: 1 },
{ id: sid('s1'), title: 'child', running: false, updatedAt: 1, parentId: sid('root') },
{ id: sid('root'), title: 'proj', displayTitle: 'proj', running: false, updatedAt: 1 },
{ id: sid('s1'), title: 'child', displayTitle: 'child', running: false, updatedAt: 1, parentId: sid('root') },
]
const rendered: string[] = []
const ui = render(