Skip to content
Draft
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
18 changes: 17 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,27 @@ Guidelines:
- The app owns visible, screen-local state: which actions are available, which
element should be spotlighted, and how actions are choreographed so users can
see control happen.
- Controllers such as MCP bridges, test harnesses, or optional external drivers should
- Controllers such as OpenAI Realtime, MCP bridges, or test harnesses should
call the app control surface instead of reaching into app internals.
- OpenAI Realtime is one replaceable control driver, not the owner of the
control architecture. The generic app-control registry and session actions
should remain useful if the voice driver is removed or replaced by tests,
scripts, MCP bridges, or other controllers.
- Provider/API secrets and privileged filesystem or server mutations remain
server-owned; the app control surface should route those through OpenWork
server APIs rather than adding provider-specific behavior to the UI.
- `/remote/session` is the OpenWork server endpoint that brokers short-lived
remote-control Realtime sessions. It keeps provider API keys server-side and
returns only an ephemeral browser client secret.
- Realtime control is a Feature Preview capability and is off by default. When
enabled, users start or stop it from the session status bar instead of a
floating overlay.
- The OpenAI key used for the initial Realtime controller can come from the
server process environment or from the OpenWork local environment store via
Settings -> Feature Preview; the browser never receives the long-lived key.
- Realtime remote control captures microphone audio in the app/browser only
after the user starts the mode. The first implementation sends audio input to
the Realtime session while keeping model output text/tool-call based.
- Raw screenshot or coordinate-based control is a fallback for uninstrumented
surfaces, not the default architecture.

Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/app/lib/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ declare global {
openExternal?: (url: string) => Promise<void>;
relaunch?: () => Promise<void>;
};
permissions?: {
requestMicrophone?: () => Promise<{ granted: boolean; status: string }>;
};
migration?: {
readSnapshot?: () => Promise<unknown>;
ackSnapshot?: () => Promise<{ ok: boolean; moved: boolean }>;
Expand Down
32 changes: 32 additions & 0 deletions apps/app/src/app/lib/openwork-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,20 @@ export type OpenworkBlueprintSessionsMaterializeResult = {
openSessionId: string | null;
};

export type OpenworkRemoteSession = {
clientSecret: string;
expiresAt: number | null;
model: string;
voice: string;
tools: string[];
};

export type OpenworkRemoteSessionRequest = {
model?: string;
voice?: string;
instructions?: string;
};

export type OpenworkArtifactItem = {
id: string;
name?: string;
Expand Down Expand Up @@ -766,6 +780,24 @@ export function createOpenworkServerClient(options: { baseUrl: string; token?: s
requestJson<OpenworkRuntimeSnapshot>(baseUrl, "/runtime/versions", { token, hostToken, timeoutMs: timeouts.status }),
status: () => requestJson<OpenworkServerDiagnostics>(baseUrl, "/status", { token, hostToken, timeoutMs: timeouts.status }),
capabilities: () => requestJson<OpenworkServerCapabilities>(baseUrl, "/capabilities", { token, hostToken, timeoutMs: timeouts.capabilities }),
createRemoteSession: (payload: OpenworkRemoteSessionRequest = {}) =>
requestJson<OpenworkRemoteSession>(baseUrl, "/remote/session", {
token,
hostToken,
method: "POST",
body: payload,
timeoutMs: timeouts.status,
}).catch((error) => {
if (error instanceof OpenworkServerError && error.status === 404) {
throw new OpenworkServerError(
404,
"remote_session_unavailable",
"Realtime control requires a newer OpenWork server. Restart OpenWork so the updated server binary is used, then try Control again.",
error.details,
);
}
throw error;
}),
listWorkspaces: () => requestJson<OpenworkWorkspaceList>(baseUrl, "/workspaces", { token, hostToken, timeoutMs: timeouts.listWorkspaces }),
createLocalWorkspace: (payload: { folderPath: string; name: string; preset: string }) =>
requestJson<WorkspaceList>(baseUrl, "/workspaces/local", {
Expand Down
1 change: 1 addition & 0 deletions apps/app/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export type SettingsTab =
| "skills"
| "extensions"
| "environment"
| "feature-preview"
| "advanced"
| "appearance"
| "updates"
Expand Down
19 changes: 19 additions & 0 deletions apps/app/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1717,13 +1717,15 @@ export default {
"settings.environment.validation_shape": "Use letters, digits, and underscores; do not start with a digit.",
"settings.environment.value_label": "Value",
"settings.tab_description_environment": "Save API keys and tokens for local agents, skills, and MCP servers. Secrets stay on this device.",
"settings.tab_description_feature_preview": "Try experimental OpenWork capabilities before they graduate into the default product.",
"settings.tab_description_messaging": "Configure router identities and inbox behavior from workspace settings.",
"settings.tab_description_model": "Tune the default model, runtime behavior, and assistant output settings.",
"settings.tab_description_recovery": "Repair migration state, reset workspace defaults, and recover local settings.",
"settings.tab_description_skills": "Browse, edit, and install skills without leaving settings.",
"settings.tab_description_updates": "Keep the app current with quiet background checks and install controls.",
"settings.tab_environment": "Environment",
"settings.tab_extensions": "Extensions",
"settings.tab_feature_preview": "Feature Preview",
"settings.tab_general": "Settings",
"settings.tab_messaging": "Messaging",
"settings.tab_model": "Model",
Expand All @@ -1734,6 +1736,23 @@ export default {
"settings.theme_light": "Light",
"settings.theme_system": "System",
"settings.theme_system_hint": "System mode follows your OS preference automatically.",
"settings.feature_preview.badge": "Preview",
"settings.feature_preview.checking": "Checking…",
"settings.feature_preview.configured": "Configured",
"settings.feature_preview.connect_server_hint": "Connect to an OpenWork server before saving a key.",
"settings.feature_preview.disabled": "Disabled",
"settings.feature_preview.enabled": "Enabled",
"settings.feature_preview.not_configured": "Not configured",
"settings.feature_preview.openai_key_description": "The key is saved in OpenWork's local environment store and used server-side to mint short-lived Realtime sessions. It is not sent to the browser as a long-lived secret.",
"settings.feature_preview.openai_key_hint": "Required for Realtime control. Existing shell OPENAI_API_KEY values still work.",
"settings.feature_preview.openai_key_label": "OpenAI API key",
"settings.feature_preview.openai_key_removed": "OpenAI API key removed.",
"settings.feature_preview.openai_key_required": "Enter an OpenAI API key before saving.",
"settings.feature_preview.openai_key_saved": "OpenAI key saved. Realtime control can use it immediately.",
"settings.feature_preview.openai_key_title": "OpenAI Realtime key",
"settings.feature_preview.realtime_description": "Shows the Realtime control entry in the session status bar. When started, OpenWork captures microphone audio and lets the Realtime model drive registered app actions.",
"settings.feature_preview.realtime_title": "Realtime control mode",
"settings.feature_preview.replace_key": "Replace OpenAI API key",
"settings.toolbar_ready_to_install": "Ready to install",
"settings.update": "Update",
"settings.update_available": "Update available: v",
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/react-app/domains/session/chat/session-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { WorkspaceSessionList } from "../sidebar/workspace-session-list";
import { SessionSurface, type SessionSurfaceProps } from "../surface/session-surface";
import { ShareWorkspaceModal } from "../../workspace/share-workspace-modal";
import { StatusBar, type StatusBarProps } from "./status-bar";
import { OpenAIRealtimeActivityPanel } from "../../../shell/control-drivers/openai-realtime/openai-realtime-activity-panel";
import {
DEFAULT_WORKSPACE_LEFT_SIDEBAR_WIDTH,
useWorkspaceShellLayout,
Expand Down Expand Up @@ -527,6 +528,8 @@ export function SessionPage(props: SessionPageProps) {
showSettingsButton={props.statusBar?.showSettingsButton}
/>
</main>

<OpenAIRealtimeActivityPanel />
</div>

{props.providerAuthModal ? <ProviderAuthModal {...props.providerAuthModal} /> : null}
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/react-app/domains/session/chat/status-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BookOpen, MessageCircle, Settings } from "lucide-react";
import { t } from "../../../../i18n";
import { usePlatform } from "../../../kernel/platform";
import { useControlAction, type OpenworkControlAction } from "../../../shell/control/control-provider";
import { OpenAIRealtimeStatusControl } from "../../../shell/control-drivers/openai-realtime/openai-realtime-status-control";
import type { OpenworkServerStatus } from "../../../../app/lib/openwork-server";

const DOCS_URL = "https://openworklabs.com/docs";
Expand Down Expand Up @@ -176,6 +177,7 @@ export function StatusBar(props: StatusBarProps) {
</div>

<div className="flex items-center gap-1.5">
<OpenAIRealtimeStatusControl />
<button
ref={docsButtonRef}
type="button"
Expand Down
Loading
Loading