mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-26 17:38:36 +00:00
feat(desktop): open a tab from the sidebar "+" when a chat is loaded
The sidebar "+" ran startFreshSessionDraft, so it replaced whatever was on screen — ⌘N behavior. With a conversation already open (possibly mid-turn) that discards it to make room for a blank draft, which is not what a create affordance should do. The tab-strip "+" and ⌘T already stack a new tab. Route the sidebar button through the same path once a session is loaded, falling back to the fresh-draft path when the surface is empty and there is no tab worth preserving. openNewSessionTile now takes an optional cwd so the new tab stays anchored to the clicked project/worktree lane.
This commit is contained in:
parent
1aed1f7bf4
commit
bd86ce9938
4 changed files with 52 additions and 7 deletions
|
|
@ -89,7 +89,7 @@ import { useRouteResume } from '../session/hooks/use-route-resume'
|
|||
import { useSessionActions } from '../session/hooks/use-session-actions'
|
||||
import { useSessionListActions } from '../session/hooks/use-session-list-actions'
|
||||
import { useSessionStateCache } from '../session/hooks/use-session-state-cache'
|
||||
import { startWorkspaceSession } from '../session/workspace-session-target'
|
||||
import { newSessionOpensTab, startWorkspaceSession } from '../session/workspace-session-target'
|
||||
import { useOverlayRouting } from '../shell/hooks/use-overlay-routing'
|
||||
import { useWindowControlsOverlayWidth } from '../shell/hooks/use-window-controls-overlay-width'
|
||||
import { titlebarControlsPosition } from '../shell/titlebar'
|
||||
|
|
@ -484,8 +484,16 @@ export function ContribWiring({ children }: { children: ReactNode }) {
|
|||
// New session anchored to a workspace (sidebar "+" on a project/worktree).
|
||||
// Seeds cwd + branch from the clicked workspace; an explicit worktree path
|
||||
// also drills the sidebar into that project so the new lane is visible.
|
||||
// Once a chat is loaded the button opens a TAB instead of replacing it —
|
||||
// see newSessionOpensTab.
|
||||
const startSessionInWorkspace = useCallback(
|
||||
(path: null | string) => {
|
||||
if (newSessionOpensTab(activeSessionIdRef.current, $selectedStoredSessionId.get())) {
|
||||
void openNewSessionTile('center', { cwd: path, listed: false })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
startWorkspaceSession({
|
||||
activeSessionIdRef,
|
||||
followActiveSessionCwd,
|
||||
|
|
@ -495,7 +503,7 @@ export function ContribWiring({ children }: { children: ReactNode }) {
|
|||
startFreshSessionDraft
|
||||
})
|
||||
},
|
||||
[activeSessionIdRef, requestGateway, startFreshSessionDraft]
|
||||
[activeSessionIdRef, openNewSessionTile, requestGateway, startFreshSessionDraft]
|
||||
)
|
||||
|
||||
// Composer "branch off into a new worktree": open a fresh session anchored
|
||||
|
|
|
|||
|
|
@ -476,13 +476,14 @@ export function useSessionActions({
|
|||
* list (Cursor-style draft tab); it surfaces on the next refresh once the
|
||||
* first message persists a turn. "Open in split" keeps the listed behavior. */
|
||||
const openNewSessionTile = useCallback(
|
||||
async (dir: TileDock = 'right', options?: { listed?: boolean }) => {
|
||||
async (dir: TileDock = 'right', options?: { cwd?: null | string; listed?: boolean }) => {
|
||||
const listed = options?.listed ?? true
|
||||
|
||||
try {
|
||||
// Fresh tile → the resolved new-session cwd (project/default), not the
|
||||
// primary composer's live cwd.
|
||||
const params = await desktopSessionCreateParams(resolveNewSessionCwd().trim())
|
||||
// Fresh tile → the caller's workspace when one was named (the sidebar
|
||||
// "+" on a project/worktree lane), else the resolved new-session cwd
|
||||
// (project/default) — never the primary composer's live cwd.
|
||||
const params = await desktopSessionCreateParams((options?.cwd || resolveNewSessionCwd()).trim())
|
||||
const created = await requestGateway<SessionCreateResponse>('session.create', params)
|
||||
const stored = created.stored_session_id
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
setNewChatWorkspaceTarget
|
||||
} from '@/store/session'
|
||||
|
||||
import { startWorkspaceSession } from './workspace-session-target'
|
||||
import { newSessionOpensTab, startWorkspaceSession } from './workspace-session-target'
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
|
|
@ -21,6 +21,29 @@ function deferred<T>() {
|
|||
return { promise, resolve }
|
||||
}
|
||||
|
||||
/**
|
||||
* The sidebar "+" is a create affordance, not a discard one: with a chat
|
||||
* already loaded it must stack a tab (⌘T) rather than replace the surface
|
||||
* (⌘N), which would throw away a conversation that may still be mid-turn.
|
||||
*/
|
||||
describe('newSessionOpensTab', () => {
|
||||
it('opens a tab once a conversation is on screen', () => {
|
||||
expect(newSessionOpensTab('runtime-a', 'stored-a')).toBe(true)
|
||||
})
|
||||
|
||||
it('opens a tab for a live runtime whose stored id has not landed yet', () => {
|
||||
expect(newSessionOpensTab('runtime-a', null)).toBe(true)
|
||||
})
|
||||
|
||||
it('opens a tab for a selected session still resuming into a runtime', () => {
|
||||
expect(newSessionOpensTab(null, 'stored-a')).toBe(true)
|
||||
})
|
||||
|
||||
it('replaces the empty surface when nothing is open', () => {
|
||||
expect(newSessionOpensTab(null, null)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('startWorkspaceSession', () => {
|
||||
afterEach(() => {
|
||||
setCurrentBranch('')
|
||||
|
|
|
|||
|
|
@ -17,6 +17,19 @@ interface WorkspaceSessionOptions {
|
|||
startFreshSessionDraft: (options?: { workspaceTarget: string }) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the sidebar "+" open a NEW TAB rather than replace what's on screen?
|
||||
*
|
||||
* "+" is a create affordance, never a discard one. Once a conversation is
|
||||
* loaded, replacing it with a blank draft would throw away a chat that may
|
||||
* still be mid-turn, so the button stacks a tab beside it (⌘T) instead of
|
||||
* taking over the surface (⌘N). With nothing open there is no tab worth
|
||||
* preserving and the cheaper fresh-draft path applies.
|
||||
*/
|
||||
export function newSessionOpensTab(activeSessionId: null | string, selectedStoredSessionId: null | string): boolean {
|
||||
return Boolean(activeSessionId || selectedStoredSessionId)
|
||||
}
|
||||
|
||||
export function startWorkspaceSession({
|
||||
activeSessionIdRef,
|
||||
followActiveSessionCwd: followCwd = followActiveSessionCwd,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue