diff --git a/apps/desktop/electron/backend-ready.test.ts b/apps/desktop/electron/backend-ready.test.ts index 9919570971..8a1079b00a 100644 --- a/apps/desktop/electron/backend-ready.test.ts +++ b/apps/desktop/electron/backend-ready.test.ts @@ -335,7 +335,12 @@ test('the merged-tail seed recovers a sentinel spliced onto a partial stderr lin test('the merged-tail seed does not match prose that merely names the sentinel', async () => { const child = makeFakeChild() - const wait = waitForDashboardPort(child, 50, () => '', () => 'still waiting for HERMES_BACKEND_READY from the backend\n') + const wait = waitForDashboardPort( + child, + 50, + () => '', + () => 'still waiting for HERMES_BACKEND_READY from the backend\n' + ) await assert.rejects(wait, /Timed out waiting/) }) diff --git a/apps/desktop/electron/remote-lifecycle.ts b/apps/desktop/electron/remote-lifecycle.ts index 1dd5082748..112fdddda3 100644 --- a/apps/desktop/electron/remote-lifecycle.ts +++ b/apps/desktop/electron/remote-lifecycle.ts @@ -36,7 +36,7 @@ const LOCKFILE_SCHEMA_VERSION = 2 // an old running dashboard unsafe to reattach to (token handling, readiness/spawn // args, served-token reconciliation). A mismatch forces a clean respawn. const PROTOCOL_VERSION = 1 -const READY_RE = READY_IN_MERGED_OUTPUT_RE // the remote log is `>> log 2>&1`: merged, not line-accurate +const READY_RE = READY_IN_MERGED_OUTPUT_RE // the remote log is `>> log 2>&1`: merged, not line-accurate const REMOTE_LOCK_DIR = '~/.hermes/desktop-ssh' const SUPPORTED_REMOTE_OS = new Set(['Linux', 'Darwin']) const DEFAULT_READY_TIMEOUT_MS = 45_000 diff --git a/apps/desktop/src/app/chat/composer/directive-actions.test.tsx b/apps/desktop/src/app/chat/composer/directive-actions.test.tsx index 4491e8b97a..b61a598538 100644 --- a/apps/desktop/src/app/chat/composer/directive-actions.test.tsx +++ b/apps/desktop/src/app/chat/composer/directive-actions.test.tsx @@ -153,7 +153,9 @@ describe('ComposerDirectiveActions', () => { const button = screen.getByRole('button') fireEvent.pointerOut(button, { relatedTarget: button.firstElementChild }) - act(() => { vi.advanceTimersByTime(750) }) + act(() => { + vi.advanceTimersByTime(750) + }) expect(pillValue()).toBe('https://example.com') }) @@ -167,9 +169,13 @@ describe('ComposerDirectiveActions', () => { fireEvent.pointerOut(chip, { relatedTarget: null }) fireEvent.mouseEnter(screen.getByRole('button').parentElement!) fireEvent.mouseLeave(screen.getByRole('button').parentElement!) - act(() => { vi.advanceTimersByTime(300) }) + act(() => { + vi.advanceTimersByTime(300) + }) fireEvent.pointerOut(document.body, { relatedTarget: document.documentElement }) - act(() => { vi.advanceTimersByTime(200) }) + act(() => { + vi.advanceTimersByTime(200) + }) expect(pillValue()).toBeNull() }) diff --git a/apps/desktop/src/app/chat/sidebar/projects/entered-content.tsx b/apps/desktop/src/app/chat/sidebar/projects/entered-content.tsx index 18024413c1..ad970ec0dd 100644 --- a/apps/desktop/src/app/chat/sidebar/projects/entered-content.tsx +++ b/apps/desktop/src/app/chat/sidebar/projects/entered-content.tsx @@ -135,7 +135,8 @@ function RepoFlatSection({ // Discovery may resurrect a removed worktree, never an explicit sidebar hide. const ordered = overlaidGroups.filter( group => - group.isMain || !dismissedWorktrees.includes(group.id) || + group.isMain || + !dismissedWorktrees.includes(group.id) || (removedWorktrees.includes(group.id) && group.path && discoveredWorktreePaths.has(group.path)) ) diff --git a/apps/desktop/src/app/chat/sidebar/sessions-section-new-session-drag.test.tsx b/apps/desktop/src/app/chat/sidebar/sessions-section-new-session-drag.test.tsx index 58f3dc4903..89c5482aea 100644 --- a/apps/desktop/src/app/chat/sidebar/sessions-section-new-session-drag.test.tsx +++ b/apps/desktop/src/app/chat/sidebar/sessions-section-new-session-drag.test.tsx @@ -154,7 +154,11 @@ function renderEnteredProjectWithLiveWorktree() { null} - repoWorktrees={{ '/repo': [{ branch: 'feature', detached: false, isMain: false, locked: false, path: '/repo/.worktrees/feature' }] }} + repoWorktrees={{ + '/repo': [ + { branch: 'feature', detached: false, isMain: false, locked: false, path: '/repo/.worktrees/feature' } + ] + }} /> ) } @@ -446,7 +450,6 @@ describe('flat-list date-divider new-session drag source', () => { }) }) - describe('explicit worktree dismissal', () => { it('resurfaces removed worktrees on discovery but not a subsequent explicit hide', async () => { dismissWorktree('/repo/.worktrees/feature', { removed: true }) diff --git a/apps/desktop/src/app/shell/context-usage-panel.test.tsx b/apps/desktop/src/app/shell/context-usage-panel.test.tsx index aacf62bf26..ab89c9a0cb 100644 --- a/apps/desktop/src/app/shell/context-usage-panel.test.tsx +++ b/apps/desktop/src/app/shell/context-usage-panel.test.tsx @@ -106,6 +106,7 @@ describe('ContextUsagePanel', () => { const { container, unmount } = render( ) + const header = container.querySelector('[data-slot="context-usage-panel"] > div')?.textContent ?? '' expect(header.includes('~')).toBe(estimated) diff --git a/apps/desktop/src/app/shell/context-usage-panel.tsx b/apps/desktop/src/app/shell/context-usage-panel.tsx index 2c3fcc0f50..d98517fb20 100644 --- a/apps/desktop/src/app/shell/context-usage-panel.tsx +++ b/apps/desktop/src/app/shell/context-usage-panel.tsx @@ -40,11 +40,17 @@ export function ContextUsagePanel({ breakdown, loading, usage }: ContextUsagePan

{copy.title}

- {copy.tokenSummary(`${usage.context_estimated ? "~" : ""}${compactNumber(contextUsed)}`, compactNumber(contextMax))} + {copy.tokenSummary( + `${usage.context_estimated ? '~' : ''}${compactNumber(contextUsed)}`, + compactNumber(contextMax) + )} -

{usage.context_estimated ? '~' : ''}{copy.percentFull(contextPercent)}

+

+ {usage.context_estimated ? '~' : ''} + {copy.percentFull(contextPercent)} +

diff --git a/apps/desktop/src/lib/statusbar.tsx b/apps/desktop/src/lib/statusbar.tsx index 3bcb643bd4..4f875fa90e 100644 --- a/apps/desktop/src/lib/statusbar.tsx +++ b/apps/desktop/src/lib/statusbar.tsx @@ -43,7 +43,7 @@ export function contextBar(percent: number | undefined, width = 10): string { export function usageContextLabel(usage: UsageStats): string { if (usage.context_max) { - return `${usage.context_estimated ? "~" : ""}${compactNumber(usage.context_used ?? 0)}/${compactNumber(usage.context_max)}` + return `${usage.context_estimated ? '~' : ''}${compactNumber(usage.context_used ?? 0)}/${compactNumber(usage.context_max)}` } return usage.total > 0 ? `${compactNumber(usage.total)} tok` : '' diff --git a/apps/desktop/src/store/gateway-profile-only-owner.integration.test.ts b/apps/desktop/src/store/gateway-profile-only-owner.integration.test.ts index c135806d9a..ecb1eab337 100644 --- a/apps/desktop/src/store/gateway-profile-only-owner.integration.test.ts +++ b/apps/desktop/src/store/gateway-profile-only-owner.integration.test.ts @@ -62,6 +62,7 @@ const { $profiles } = await import('./profile') const { $activeSessionId, _resetSessionOwnerHintsForTests, setSessionOwnerHint, setSessions } = await import('./session') + const { $gateway } = await import('./gateway') const { clearAllPrompts } = await import('./prompts') diff --git a/apps/desktop/src/store/layout.ts b/apps/desktop/src/store/layout.ts index 374b23e24c..e9b2676a22 100644 --- a/apps/desktop/src/store/layout.ts +++ b/apps/desktop/src/store/layout.ts @@ -208,11 +208,7 @@ export const $dismissedWorktreeIds = persistentAtom( ) // Only successful git removals may reappear on discovery. Explicit hides, // including legacy dismissals without provenance, remain hidden. -export const $removedWorktreeIds = persistentAtom( - 'hermes.desktop.removedWorktrees', - [] as string[], - Codecs.stringArray -) +export const $removedWorktreeIds = persistentAtom('hermes.desktop.removedWorktrees', [] as string[], Codecs.stringArray) export const $sidebarPinsOpen = atom(true) export const $sidebarRecentsOpen = atom(true) // Cron-job sessions live in their own section below recents, collapsed by diff --git a/apps/desktop/src/store/onboarding.test.ts b/apps/desktop/src/store/onboarding.test.ts index e52aca39d6..0f2652e748 100644 --- a/apps/desktop/src/store/onboarding.test.ts +++ b/apps/desktop/src/store/onboarding.test.ts @@ -83,6 +83,7 @@ describe('refreshOnboarding', () => { it('keeps onboarding work in its initiating lifetime and profile', async () => { const { startManualOnboarding, startProviderOAuth, saveOnboardingApiKey, closeManualOnboarding } = await import('./onboarding') + const requests: { path: string; profile?: string }[] = [] let release!: () => void let delayKey = true diff --git a/apps/desktop/src/store/onboarding.ts b/apps/desktop/src/store/onboarding.ts index 486655648e..ea399b576a 100644 --- a/apps/desktop/src/store/onboarding.ts +++ b/apps/desktop/src/store/onboarding.ts @@ -344,6 +344,7 @@ async function completeWithModelConfirm( if (generation !== flowGeneration) { return } + const defaults = await fetchProviderDefaultModel(preferredSlugs, ctx.profile) if (generation !== flowGeneration) { @@ -369,11 +370,13 @@ async function completeWithModelConfirm( if (generation !== flowGeneration) { return } + notifyGatewayTools(res.gateway_tools) } catch (error) { if (generation !== flowGeneration) { return } + onFail(error instanceof Error ? error.message : 'Hermes could not save the selected model.') return @@ -433,11 +436,13 @@ async function refreshProviders() { if (generation !== flowGeneration) { return } + patch({ mode: providers.length > 0 ? 'oauth' : 'apikey', providers }) } catch { if (generation !== flowGeneration) { return } + patch({ mode: 'apikey', providers: [] }) } finally { if (generation === flowGeneration) { @@ -725,6 +730,7 @@ export async function startProviderOAuth(provider: OAuthProvider, ctx: Onboardin if (generation !== flowGeneration) { return } + setFlow({ status: 'error', provider, message: `Could not start sign-in: ${errMessage(error)}` }) } } @@ -756,6 +762,7 @@ async function pollSession(provider: OAuthProvider, start: DeviceStart, ctx: Onb if (generation !== flowGeneration) { return } + clearPoll() setFlow({ status: 'error', provider, start, message: `Polling failed: ${errMessage(error)}` }) } @@ -805,6 +812,7 @@ export async function submitOnboardingCode(ctx: OnboardingContext) { if (generation !== flowGeneration) { return } + setFlow({ status: 'error', provider, start, message: errMessage(error) }) } } @@ -925,6 +933,7 @@ export async function saveOnboardingApiKey( if (generation !== flowGeneration) { return { ok: false } } + // For API-key flows we don't have a definitive provider id (the // user picked which API key they're entering, but the corresponding // backend slug — e.g. OPENROUTER_API_KEY → "openrouter" — is the @@ -1008,6 +1017,7 @@ export async function saveOnboardingLocalEndpoint(baseUrl: string, apiKey: strin if (generation !== flowGeneration) { return { ok: false } } + await ctx.requestGateway('reload.env').catch(() => undefined) if (generation !== flowGeneration) { @@ -1064,6 +1074,7 @@ export async function setOnboardingModel(model: string) { if (generation !== flowGeneration) { return } + const current = $desktopOnboarding.get().flow if (current.status === 'confirming_model') { @@ -1073,6 +1084,7 @@ export async function setOnboardingModel(model: string) { if (generation !== flowGeneration) { return } + notifyError(error, 'Could not change model') const current = $desktopOnboarding.get().flow diff --git a/ui-tui/src/__tests__/appChromeStatusRule.test.tsx b/ui-tui/src/__tests__/appChromeStatusRule.test.tsx index 06a6883c5a..8b8ce42852 100644 --- a/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +++ b/ui-tui/src/__tests__/appChromeStatusRule.test.tsx @@ -108,11 +108,14 @@ describe('StatusRule session title', () => { it('marks only estimated context occupancy at every visible width', () => { for (const cols of [80, 120, 200]) { for (const estimated of [true, false]) { - const text = textContent(StatusRule({ - ...baseProps, cols, - statusBarFields: new Set(['context_detail']), - usage: { ...baseProps.usage, context_estimated: estimated } - })) + const text = textContent( + StatusRule({ + ...baseProps, + cols, + statusBarFields: new Set(['context_detail']), + usage: { ...baseProps.usage, context_estimated: estimated } + }) + ) const context = text.match(/(~?\d+(?:\.\d+)?k(?:\/\d+k| tok))/)?.[1] diff --git a/ui-tui/src/app/slash/commands/session.ts b/ui-tui/src/app/slash/commands/session.ts index ee6da350fb..5367b5a1b9 100644 --- a/ui-tui/src/app/slash/commands/session.ts +++ b/ui-tui/src/app/slash/commands/session.ts @@ -744,7 +744,9 @@ export const sessionCommands: SlashCommand[] = [ if (r.context_max) { const mark = r.context_estimated ? '~' : '' - sections.push({ text: `Context: ${mark}${f(r.context_used)} / ${f(r.context_max)} (${mark}${r.context_percent}%)` }) + sections.push({ + text: `Context: ${mark}${f(r.context_used)} / ${f(r.context_max)} (${mark}${r.context_percent}%)` + }) } if (r.compressions) { diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx index d49f13aa0b..709a484d68 100644 --- a/ui-tui/src/components/appChrome.tsx +++ b/ui-tui/src/components/appChrome.tsx @@ -731,7 +731,8 @@ export function StatusRule({ {showBar ? ( {' │ '} - [{bar}] {pct != null ? `${contextMark}${pct}%` : ''} + [{bar}]{' '} + {pct != null ? `${contextMark}${pct}%` : ''} ) : null} {showDuration ? (