Files
WeHub Mirror 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
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

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 } : {}),
},
}
: {}),
}
}