mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(desktop): find and reuse an open blank "New session" tab
An unused draft tab is the one a user would have typed into, so give the tile store a way to name it (blankDraftTile) and hand it to another session in place (reuseBlankDraftTile). A blank-but-busy tab has its first turn in flight and an unbound tile is unknown rather than empty, so neither is a candidate.
This commit is contained in:
parent
fb120f850d
commit
272d3be6c8
2 changed files with 78 additions and 1 deletions
|
|
@ -1,8 +1,14 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ClientSessionState } from '@/app/types'
|
||||
import { group, split } from '@/components/pane-shell/tree/model'
|
||||
import type { SessionTile } from '@/store/session-states'
|
||||
import { focusedSessionNeedsRoute, orderTilesByTree, selectionHomesToWorkspace } from '@/store/session-states'
|
||||
import {
|
||||
blankDraftTile,
|
||||
focusedSessionNeedsRoute,
|
||||
orderTilesByTree,
|
||||
selectionHomesToWorkspace
|
||||
} from '@/store/session-states'
|
||||
|
||||
const tile = (storedSessionId: string): SessionTile => ({ storedSessionId })
|
||||
const tilePane = (id: string) => `session-tile:${id}`
|
||||
|
|
@ -64,3 +70,38 @@ describe('focusedSessionNeedsRoute', () => {
|
|||
expect(focusedSessionNeedsRoute('tile', false)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('blankDraftTile', () => {
|
||||
const bound = (storedSessionId: string, runtimeId: string): SessionTile => ({ runtimeId, storedSessionId })
|
||||
|
||||
const state = (messages: number, busy = false) =>
|
||||
({ busy, messages: Array.from({ length: messages }, (_, i) => ({ id: `m${i}` })) }) as ClientSessionState
|
||||
|
||||
it('finds the open tab whose session has no messages', () => {
|
||||
const tiles = [bound('a', 'run-a'), bound('b', 'run-b')]
|
||||
const states = { 'run-a': state(3), 'run-b': state(0) }
|
||||
|
||||
expect(blankDraftTile(tiles, states)).toEqual(tiles[1])
|
||||
})
|
||||
|
||||
it('picks the most recent blank tab when there are several', () => {
|
||||
const tiles = [bound('a', 'run-a'), bound('b', 'run-b')]
|
||||
const states = { 'run-a': state(0), 'run-b': state(0) }
|
||||
|
||||
expect(blankDraftTile(tiles, states)).toEqual(tiles[1])
|
||||
})
|
||||
|
||||
it('leaves a blank-but-busy tab alone — its first turn is already in flight', () => {
|
||||
expect(blankDraftTile([bound('a', 'run-a')], { 'run-a': state(0, true) })).toBeNull()
|
||||
})
|
||||
|
||||
it('treats an unbound or unpublished tile as unknown, not empty', () => {
|
||||
expect(blankDraftTile([tile('a')], {})).toBeNull()
|
||||
expect(blankDraftTile([bound('a', 'run-a')], {})).toBeNull()
|
||||
})
|
||||
|
||||
it('is null when every open tab holds a conversation', () => {
|
||||
expect(blankDraftTile([bound('a', 'run-a')], { 'run-a': state(2) })).toBeNull()
|
||||
expect(blankDraftTile([], {})).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -636,6 +636,42 @@ export function focusedSessionNeedsRoute(focused: 'main' | 'tile' | null, worksp
|
|||
return !focused || (focused === 'main' && workspaceIsPage)
|
||||
}
|
||||
|
||||
/** The open tab that's still an empty "New session" draft, if there is one.
|
||||
* That tab is the one the user would have typed into, so an open-from-nowhere
|
||||
* spends it instead of stacking a second blank tab beside it. Most recent
|
||||
* wins; a tile whose runtime hasn't bound (or whose state hasn't published) is
|
||||
* unknown rather than empty, so it's left alone. */
|
||||
export function blankDraftTile(
|
||||
tiles: readonly SessionTile[],
|
||||
states: Record<string, ClientSessionState>
|
||||
): null | SessionTile {
|
||||
return (
|
||||
tiles.findLast(({ runtimeId }) => {
|
||||
const state = runtimeId ? states[runtimeId] : undefined
|
||||
|
||||
return Boolean(state && !state.busy && state.messages.length === 0)
|
||||
}) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
/** Hand an open blank draft tab over to `storedSessionId`, keeping its slot.
|
||||
* False when there's no such tab, so the caller can fall back. The spent draft
|
||||
* is DISCARDED rather than closed: it never held a conversation, so ⌘⇧T
|
||||
* resurrecting it would just restore an empty tab. */
|
||||
export function reuseBlankDraftTile(storedSessionId: string): boolean {
|
||||
const tile = blankDraftTile($sessionTiles.get(), $sessionStates.get())
|
||||
|
||||
if (!tile || tile.storedSessionId === storedSessionId) {
|
||||
return false
|
||||
}
|
||||
|
||||
discardSessionTile(tile.storedSessionId)
|
||||
openSessionTile(storedSessionId, tile.dir, tile.anchor, tile.before)
|
||||
revealTreePane(`${TILE_PANE_PREFIX}${storedSessionId}`)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Closed-tab stack for ⌘⇧T reopen (in-memory) — keyed PER PROFILE like the
|
||||
// tiles themselves, so ⌘⇧T after a profile switch never resurrects the other
|
||||
// profile's session. The tile's placement is remembered so it returns in place.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue