Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/main/ipc/dashboard-payload-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
DASHBOARD_MAX_LABEL_LENGTH,
type DashboardRevealAgentArgs,
type DashboardSleepWorkspaceArgs,
type DashboardSnapshot
type DashboardSnapshot,
type DashboardStopAgentArgs
} from '../../shared/dashboard-snapshot'
import { BoundedMap } from '../../shared/bounded-map'
import { normalizeExecutionHostId } from '../../shared/execution-host'
Expand Down Expand Up @@ -83,6 +84,20 @@ export function isDashboardRevealAgentArgs(value: unknown): value is DashboardRe
)
}

export function isDashboardStopAgentArgs(value: unknown): value is DashboardStopAgentArgs {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false
}
const args = value as Record<string, unknown>
return (
isBoundedString(args.paneKey, MAX_ID_LENGTH) &&
isBoundedString(args.worktreeId, MAX_ID_LENGTH) &&
isBoundedString(args.tabId, MAX_ID_LENGTH) &&
(args.leafId === null || isBoundedString(args.leafId, MAX_ID_LENGTH)) &&
(args.ptyId === null || isBoundedString(args.ptyId, MAX_ID_LENGTH))
)
}

export function isDashboardPaneKey(value: unknown): value is string {
return isBoundedString(value, MAX_ID_LENGTH)
}
Expand Down
35 changes: 35 additions & 0 deletions src/main/ipc/dashboard-popout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,41 @@ describe('registerDashboardPopoutHandlers', () => {
expect(sendToTrustedMock).toHaveBeenCalledWith('ui:sleepDashboardWorkspace', args)
})

it('relays valid stop requests from only the popout, without raising the main window', () => {
const args = {
paneKey: 'tab1:leaf1',
worktreeId: 'w1',
tabId: 't1',
leafId: 'l1',
ptyId: 'pty1'
}

handlers.get('dashboardPopout:stopAgent')!({ sender: untrustedSender } as never, args)
handlers.get('dashboardPopout:stopAgent')!({ sender: popoutSender } as never, {
...args,
paneKey: ''
})
expect(sendToTrustedMock).not.toHaveBeenCalled()

handlers.get('dashboardPopout:stopAgent')!({ sender: popoutSender } as never, args)
expect(sendToTrustedMock).toHaveBeenCalledWith('ui:stopDashboardAgent', args)
// Stopping must not pull the user off the board.
expect(safelyRevealMock).not.toHaveBeenCalled()
expect(appMock.focus).not.toHaveBeenCalled()
})

it('accepts a stop request for a retained card with no live pane', () => {
const args = {
paneKey: 'tab1:leaf1',
worktreeId: 'w1',
tabId: 't1',
leafId: null,
ptyId: null
}

handlers.get('dashboardPopout:stopAgent')!({ sender: popoutSender } as never, args)
expect(sendToTrustedMock).toHaveBeenCalledWith('ui:stopDashboardAgent', args)
})
it('reveals an agent in only the trusted main window', () => {
const main = makeWindow(mainSender)
getTrustedWindowMock.mockReturnValue(main)
Expand Down
18 changes: 17 additions & 1 deletion src/main/ipc/dashboard-popout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
isDashboardPaneKey,
isDashboardRevealAgentArgs,
isDashboardSleepWorkspaceArgs,
isDashboardSpawnAgentArgs
isDashboardSpawnAgentArgs,
isDashboardStopAgentArgs
} from './dashboard-payload-validation'

// The most recent snapshot the main renderer published, replayed to the popout
Expand All @@ -40,6 +41,7 @@ export function registerDashboardPopoutHandlers(
ipcMain.removeHandler('dashboardPopout:ackAgent')
ipcMain.removeHandler('dashboardPopout:spawnAgent')
ipcMain.removeHandler('dashboardPopout:sleepWorkspace')
ipcMain.removeHandler('dashboardPopout:stopAgent')

onDashboardPopoutOpenChanged((open) => {
if (!open) {
Expand Down Expand Up @@ -127,6 +129,20 @@ export function registerDashboardPopoutHandlers(
sendToTrustedUIRenderer('ui:ackDashboardAgent', (args as { paneKey: string }).paneKey)
})

// Stop-from-board: the main renderer owns the terminal/pty teardown, so relay
// the request to it. Deliberately does NOT raise the main window — the whole
// point is clearing an agent without leaving the board.
ipcMain.handle('dashboardPopout:stopAgent', (event, args: unknown): void => {
if (
!isDashboardPopoutRenderer(event.sender) ||
!isDashboardEnabled(store) ||
!isDashboardStopAgentArgs(args)
) {
return
}
sendToTrustedUIRenderer('ui:stopDashboardAgent', args)
})

// Click-to-focus: raise the main window and route it to the agent's pane.
ipcMain.handle('dashboardPopout:revealAgent', (event, args: unknown): void => {
if (
Expand Down
53 changes: 53 additions & 0 deletions src/main/rate-limits/antigravity-auth-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { UsageRateLimitFailureKind } from '../../shared/rate-limit-types'

export type AntigravityCredentialSource = 'official-keychain' | 'official-token-file'

export type AntigravityAccessToken = {
accessToken: string
credentialSource: AntigravityCredentialSource
sourceKey: string
}

export class AntigravityAuthError extends Error {
readonly failureKind: UsageRateLimitFailureKind
readonly status: number | null

constructor(
message: string,
failureKind: UsageRateLimitFailureKind,
status: number | null = null
) {
super(message)
this.name = 'AntigravityAuthError'
this.failureKind = failureKind
this.status = status
}
}

export type TokenEnvelope = {
token: {
access_token: string
refresh_token?: string
token_type?: string
expiry?: string | number
}
[key: string]: unknown
}

export type ParsedCredentials = {
source: AntigravityCredentialSource
sourceKey: string
tokenPath: string | null
envelope: TokenEnvelope
accessToken: string
refreshToken: string
expiresAtMs: number | null
baseHomeDir: string
}

export type RefreshedCredentials = {
accessToken: string
refreshToken: string
expiresAtMs: number
idToken?: string
}
Loading