6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
196 lines
7.1 KiB
TypeScript
196 lines
7.1 KiB
TypeScript
/**
|
|
* THE single detection point for the Sim desktop app.
|
|
*
|
|
* The web app is served identically to browsers and to the desktop shell; the
|
|
* only difference is the preload bridge the shell injects
|
|
* (`window.simDesktop`, typed in `@sim/desktop-bridge`). Desktop features are
|
|
* progressive enhancements: check for the bridge here, never assume it.
|
|
*
|
|
* Rules for scaling desktop features without scattering gates:
|
|
* - Shared code must not touch `window.simDesktop` directly — add an accessor
|
|
* here (or a feature-scoped wrapper like `lib/browser-agent/transport.ts`
|
|
* that builds on {@link getDesktopBridge}) so "everything desktop" stays
|
|
* greppable from one module.
|
|
* - Gate through a feature-named helper (e.g. {@link hasLocalFilesystem}) so
|
|
* callers remain explicit about what they need.
|
|
* - The browser and terminal additionally have a device switch the user can
|
|
* turn off. `has*` answers whether the shell ships the surface (what the
|
|
* settings pages need, so the switch can be turned back on); `is*Enabled`
|
|
* answers whether it may actually run (what everything else needs).
|
|
* - Anything advertised to the copilot backend must flow through
|
|
* {@link getDesktopChatCapabilities} so every chat surface reports
|
|
* capabilities consistently.
|
|
*/
|
|
import type { BrowserKnownSession } from '@sim/browser-protocol'
|
|
import type { DesktopPreferences, SimDesktopApi } from '@sim/desktop-bridge'
|
|
|
|
/** The preload bridge, or undefined outside the desktop app (and on the server). */
|
|
export function getDesktopBridge(): SimDesktopApi | undefined {
|
|
if (typeof window === 'undefined') return undefined
|
|
return window.simDesktop
|
|
}
|
|
|
|
/** True when running inside the Sim desktop app. */
|
|
export function isDesktopApp(): boolean {
|
|
return getDesktopBridge() !== undefined
|
|
}
|
|
|
|
/** True when the shell can serve read-only local-directory grants. */
|
|
export function hasLocalFilesystem(): boolean {
|
|
return isDesktopApp()
|
|
}
|
|
|
|
/** True when the shell hosts the embedded agent browser. */
|
|
export function hasBrowserAgent(): boolean {
|
|
return isDesktopApp()
|
|
}
|
|
|
|
/** True when the shell can run an interactive local shell for the agent. */
|
|
export function hasTerminal(): boolean {
|
|
return isDesktopApp()
|
|
}
|
|
|
|
/** True when the shell exposes device-level desktop preferences. */
|
|
export function hasDesktopSettings(): boolean {
|
|
return isDesktopApp()
|
|
}
|
|
|
|
/**
|
|
* The device switches for the browser and terminal, cached because the chat UI
|
|
* reads availability synchronously while the shell only answers over async
|
|
* IPC. An unread value uses the shell default (enabled).
|
|
*
|
|
* The cache is authoritative at call time, which is what tool execution and
|
|
* capability reporting need. React trees that read it in a memo settle on the
|
|
* next mount — flipping a switch happens on a settings route, so the chat view
|
|
* has unmounted by then anyway.
|
|
*/
|
|
let devicePreferences: DesktopPreferences | null = null
|
|
let devicePreferencesLoad: Promise<void> | null = null
|
|
|
|
function loadDevicePreferences(): Promise<void> {
|
|
devicePreferencesLoad ??=
|
|
getDesktopBridge()
|
|
?.settings.getPreferences()
|
|
.then((preferences) => {
|
|
devicePreferences = preferences
|
|
})
|
|
.catch(() => {}) ?? Promise.resolve()
|
|
return devicePreferencesLoad
|
|
}
|
|
|
|
function isSurfaceSwitchedOn(key: 'browserEnabled' | 'terminalEnabled'): boolean {
|
|
void loadDevicePreferences()
|
|
return devicePreferences?.[key] !== false
|
|
}
|
|
|
|
/**
|
|
* Publishes a preference change from the settings pages so the synchronous
|
|
* gates below stop lagging a round trip behind the shell.
|
|
*/
|
|
export function setDesktopPreferencesSnapshot(preferences: DesktopPreferences): void {
|
|
devicePreferences = preferences
|
|
devicePreferencesLoad = Promise.resolve()
|
|
}
|
|
|
|
/** True when the agent browser is installed and switched on for this device. */
|
|
export function isBrowserAgentEnabled(): boolean {
|
|
return hasBrowserAgent() && isSurfaceSwitchedOn('browserEnabled')
|
|
}
|
|
|
|
/** True when the agent terminal is installed and switched on for this device. */
|
|
export function isTerminalEnabled(): boolean {
|
|
return hasTerminal() && isSurfaceSwitchedOn('terminalEnabled')
|
|
}
|
|
|
|
/**
|
|
* The installed shell's semver, or undefined in a browser. Input to the
|
|
* minimum-shell-version gate (see `lib/desktop/min-version.ts`).
|
|
*/
|
|
export function getDesktopShellVersion(): string | undefined {
|
|
return getDesktopBridge()?.version
|
|
}
|
|
|
|
/** The shell updater surface, when the installed shell provides one. */
|
|
export function getDesktopUpdates(): SimDesktopApi['updates'] | undefined {
|
|
return getDesktopBridge()?.updates
|
|
}
|
|
|
|
/**
|
|
* Summary of one open shell, sent with each request so the agent knows what is
|
|
* already running without having to ask. No output and no environment: only
|
|
* enough to pick a terminal and notice when one is occupied.
|
|
*/
|
|
export interface DesktopTerminalHint {
|
|
id: string
|
|
cwd?: string
|
|
running?: string
|
|
interactive?: boolean
|
|
active?: boolean
|
|
}
|
|
|
|
export interface DesktopChatCapabilities {
|
|
desktopCapabilities?: {
|
|
localFilesystem?: true
|
|
browser?: true
|
|
terminal?: true
|
|
browserSessions?: BrowserKnownSession[]
|
|
terminals?: DesktopTerminalHint[]
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The capability fragment spread into chat request payloads. Mothership gates
|
|
* user-local VFS guidance/routing and the browser subagent on these flags, so
|
|
* in a plain web browser the model never sees the features.
|
|
*/
|
|
export async function getDesktopChatCapabilities(
|
|
scopeId: string
|
|
): Promise<DesktopChatCapabilities> {
|
|
const bridge = getDesktopBridge()
|
|
// Never advertise a surface the user switched off, even on the first
|
|
// request after launch, before the cached preferences have arrived.
|
|
await loadDevicePreferences()
|
|
const localFilesystem = hasLocalFilesystem()
|
|
const browser = isBrowserAgentEnabled()
|
|
const terminal = isTerminalEnabled()
|
|
// Sent every request so the agent knows what is already running without
|
|
// spending a tool call to ask — and, more importantly, so it notices a
|
|
// terminal that is occupied instead of launching a second copy into it.
|
|
const terminals: DesktopTerminalHint[] =
|
|
terminal && bridge
|
|
? await bridge.terminal
|
|
.getTabs(scopeId)
|
|
.then((state) =>
|
|
state.tabs.map((tab) => ({
|
|
id: tab.terminalId,
|
|
...(tab.cwd ? { cwd: tab.cwd } : {}),
|
|
...(tab.running ? { running: tab.running } : {}),
|
|
...(tab.interactive ? { interactive: true as const } : {}),
|
|
...(tab.active ? { active: true as const } : {}),
|
|
}))
|
|
)
|
|
.catch(() => [])
|
|
: []
|
|
const browserSessions =
|
|
browser && bridge
|
|
? await bridge.browserAgent
|
|
.getKnownSessions()
|
|
.then((state) => state.sessions)
|
|
.catch(() => [])
|
|
: []
|
|
return {
|
|
...(localFilesystem || browser || terminal
|
|
? {
|
|
desktopCapabilities: {
|
|
...(localFilesystem ? { localFilesystem: true as const } : {}),
|
|
...(browser ? { browser: true as const } : {}),
|
|
...(terminal ? { terminal: true as const } : {}),
|
|
...(terminals.length > 0 ? { terminals } : {}),
|
|
...(browserSessions.length > 0 ? { browserSessions } : {}),
|
|
},
|
|
}
|
|
: {}),
|
|
}
|
|
}
|