Merge remote-tracking branch 'origin/master' into mergebot/pr711

# Conflicts:
#	packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx
#	packages/client/ui-workspace/src/client/index.ts
#	packages/client/ui-workspace/src/client/rows/Rows.tsx
#	packages/client/ui-workspace/src/client/tree.ts
#	packages/client/ui-workspace/tests/apply.spec.ts
#	packages/client/ui-workspace/tests/rows.spec.tsx
#	packages/client/ui-workspace/tests/tree.spec.ts
#	packages/client/ui-workspace/tests/workspace-browser.spec.tsx
This commit is contained in:
imccyu
2026-07-31 04:37:08 +08:00
243 changed files with 3584 additions and 1336 deletions
+21 -19
View File
@@ -3,7 +3,7 @@ import type {
SessionId, SessionListState, SessionSummary, WorkspaceId, WorkspaceView,
} from '@deepseek-ai/dsh-client-runtime/client'
import {
deriveFlat, deriveGroups, deriveSearchResults, formatRelativeTime, projectLabel,
deriveFlat, deriveGroups, deriveSearchResults, projectLabel, relativeTime,
UNGROUPED_KEY, UNGROUPED_LABEL,
} from '../src/client/tree.ts'
import { createWorkspaceViewStore } from '../src/client/stores.ts'
@@ -55,7 +55,12 @@ describe('deriveGroups', () => {
sessions, [workspace('first', ['shown', 'current-blank', 'stale-blank'])], view(['first']),
)
expect(groups[0]!.sessions.map(session => session.id)).toEqual([real.id, currentBlank.id])
expect(groups[0]!.sessions.find(session => session.id === currentBlank.id)!.title).toBe('New Session')
const blankNode = groups[0]!.sessions.find(session => session.id === currentBlank.id)!
// The stored placeholder title stays canonical; the renderer swaps in
// the localized New Session label via the blank flag.
expect(blankNode.title).toBe('New Session')
expect(blankNode.blank).toBe(true)
expect(groups[0]!.sessions.find(session => session.id === real.id)!.blank).toBe(false)
expect(groups[0]!.sessionCount).toBe(2)
// A non-current blank stray never surfaces an Ungrouped bucket either.
const strayGroups = deriveGroups(list({ ...summary('stray', 2), blank: true }), [workspace('first', [])], view())
@@ -125,7 +130,7 @@ describe('deriveFlat', () => {
expect(deriveFlat(partial).map(row => row.id)).toEqual([sid('present')])
})
it('shows only the current blank session with its New Session title', () => {
it('shows only the current blank session and excludes blanks from search', () => {
const currentBlank = { ...summary('current-blank', 9), blank: true }
const staleBlank = { ...summary('stale-blank', 8), blank: true }
const sessions = {
@@ -135,6 +140,7 @@ describe('deriveFlat', () => {
const rows = deriveFlat(sessions)
expect(rows.map(row => row.id)).toEqual([currentBlank.id, sid('real')])
expect(rows.map(row => row.title)).toEqual(['New Session', 'real'])
expect(rows.map(row => row.blank)).toEqual([true, false])
})
})
@@ -193,13 +199,15 @@ describe('deriveSearchResults', () => {
})
})
it('shows only the current blank row and uses its New Session display title', () => {
it('excludes blank sessions from search regardless of query or content hits', () => {
const currentBlank = { ...summary('opaque-current', 5), blank: true }
const staleBlank = { ...summary('new session stale', 4), blank: true }
const sessions = {
...list(currentBlank, staleBlank),
current: currentBlank.id,
}
// Blank placeholders never match — not their localized-display title, not
// their id, and not even a backend content hit naming them.
const result = deriveSearchResults(
sessions,
[workspace('first', ['opaque-current', 'new session stale'])],
@@ -213,13 +221,7 @@ describe('deriveSearchResults', () => {
},
10,
)
expect(result.items).toEqual([{
id: currentBlank.id,
title: 'New Session',
workspace: 'first',
running: false,
snippet: 'current body',
}])
expect(result.items).toEqual([])
})
it('uses the supplied cap and preserves either local overflow or backend hasMore', () => {
@@ -271,14 +273,14 @@ describe('projectLabel', () => {
})
})
describe('formatRelativeTime', () => {
it('formats current, minute, hour, day, month, and year buckets', () => {
describe('relativeTime', () => {
it('buckets current, minute, hour, day, month, and year distances', () => {
const now = 400 * 24 * 60 * 60 * 1_000
expect(formatRelativeTime(now, now)).toBe('now')
expect(formatRelativeTime(now - 5 * 60_000, now)).toBe('5min')
expect(formatRelativeTime(now - 3 * 3_600_000, now)).toBe('3h')
expect(formatRelativeTime(now - 2 * 86_400_000, now)).toBe('2d')
expect(formatRelativeTime(now - 60 * 86_400_000, now)).toBe('2mo')
expect(formatRelativeTime(0, now)).toBe('1y')
expect(relativeTime(now, now)).toEqual({ unit: 'now', n: 0 })
expect(relativeTime(now - 5 * 60_000, now)).toEqual({ unit: 'minutes', n: 5 })
expect(relativeTime(now - 3 * 3_600_000, now)).toEqual({ unit: 'hours', n: 3 })
expect(relativeTime(now - 2 * 86_400_000, now)).toEqual({ unit: 'days', n: 2 })
expect(relativeTime(now - 60 * 86_400_000, now)).toEqual({ unit: 'months', n: 2 })
expect(relativeTime(0, now)).toEqual({ unit: 'years', n: 1 })
})
})