Skip to content
Merged
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
77 changes: 77 additions & 0 deletions .github/workflows/ci-openwork-ui-mcp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: openwork-ui-mcp

on:
push:
branches: [dev]
paths:
- "packages/openwork-ui-mcp/**"
- ".github/workflows/ci-openwork-ui-mcp.yml"
tags:
- "openwork-ui-mcp-v*"
pull_request:
branches: [dev]
paths:
- "packages/openwork-ui-mcp/**"
- ".github/workflows/ci-openwork-ui-mcp.yml"

permissions:
contents: read

defaults:
run:
working-directory: packages/openwork-ui-mcp

jobs:
check:
name: Syntax & dry-run publish
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
registry-url: https://registry.npmjs.org

- name: Install dependencies
run: npm install --ignore-scripts

- name: Syntax check
run: node --check index.mjs

- name: Dry-run publish
run: npm publish --dry-run --access public

publish:
name: Publish to npm
needs: check
if: startsWith(github.ref, 'refs/tags/openwork-ui-mcp-v')
runs-on: ubuntu-latest

permissions:
contents: read
id-token: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
registry-url: https://registry.npmjs.org

- name: Install dependencies
run: npm install --ignore-scripts

- name: Syntax check
run: node --check index.mjs

- name: Publish
run: npm publish --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Read `ARCHITECTURE.md` for runtime flow, server-vs-shell ownership, and architec
* **Open source**: keep the repo portable; no secrets committed.
* **Slick and fluid**: 60fps animations, micro-interactions, premium feel.
* **Mobile-native**: touch targets, gestures, and layouts optimized for small screens.
* **Provider-neutral control**: expose app actions through OpenWork-owned control surfaces first; provider-specific controllers should drive those surfaces rather than hardwiring provider logic into the app UI.

## Task Intake (Required)

Expand Down
36 changes: 36 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,42 @@ These are all opencode primitives you can read the docs to find out exactly how

OpenWork is a client experience that consumes OpenWork server surfaces.

### Provider-neutral app control surface

OpenWork app control mode is owned by the UI runtime. The app exposes a
provider-neutral action registry through `window.__openworkControl` so external
controllers can inspect the current route, discover visible/safe actions, and
request an action by ID without depending on DOM scraping or a specific model
provider.

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
call the app control surface instead of reaching into app internals.
- 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.
- Raw screenshot or coordinate-based control is a fallback for uninstrumented
surfaces, not the default architecture.

### MCP UI Control profile

OpenWork should standardize external app control through MCP where possible. The
app-local `window.__openworkControl` registry remains the source of current UI
affordances, but public integrations should expose those affordances as MCP
tools that follow `docs/mcp-ui-control-profile.md`:

- `ui.snapshot` for current semantic app state
- `ui.list_actions` for currently available action metadata and input schemas
- `ui.execute_action` for running one semantic action by ID

Standalone control clients such as HandsFree should be MCP clients first: they
can connect to any configured MCP server and call generic MCP tools. OpenWork's
local UI bridge is an implementation detail behind the OpenWork MCP surface.

OpenWork supports two product runtime modes for users:

- desktop
Expand Down
39 changes: 38 additions & 1 deletion apps/app/src/react-app/domains/session/chat/status-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/** @jsxImportSource react */
import { useEffect, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
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 type { OpenworkServerStatus } from "../../../../app/lib/openwork-server";

const DOCS_URL = "https://openworklabs.com/docs";
Expand Down Expand Up @@ -103,6 +104,9 @@ function deriveStatusCopy(props: StatusBarProps): StatusCopy {

export function StatusBar(props: StatusBarProps) {
const platform = usePlatform();
const docsButtonRef = useRef<HTMLButtonElement>(null);
const feedbackButtonRef = useRef<HTMLButtonElement>(null);
const settingsButtonRef = useRef<HTMLButtonElement>(null);
const [initializing, setInitializing] = useState(
() => Date.now() - STATUS_BAR_BOOT_STARTED_AT < STATUS_BAR_INITIALIZING_MS,
);
Expand All @@ -118,6 +122,36 @@ export function StatusBar(props: StatusBarProps) {
}, [initializing]);

const statusCopy = deriveStatusCopy({ ...props, initializing });
const docsControlAction = useMemo<OpenworkControlAction>(() => ({
id: "status.docs.open",
label: "Open OpenWork docs",
description: "Open the documentation from the status bar.",
sideEffect: "external",
targetRef: docsButtonRef,
execute: () => platform.openLink(DOCS_URL),
}), [platform]);
useControlAction(docsControlAction);

const feedbackControlAction = useMemo<OpenworkControlAction>(() => ({
id: "status.feedback.open",
label: "Send feedback",
description: "Open the OpenWork feedback surface from the status bar.",
sideEffect: "external",
targetRef: feedbackButtonRef,
execute: props.onSendFeedback,
}), [props.onSendFeedback]);
useControlAction(feedbackControlAction);

const settingsControlAction = useMemo<OpenworkControlAction>(() => ({
id: "status.settings.open",
label: props.settingsOpen ? "Go back from settings" : "Open settings from the status bar",
description: "Use the visible settings button in the status bar.",
sideEffect: "navigation",
disabled: props.showSettingsButton === false,
targetRef: settingsButtonRef,
execute: props.onOpenSettings,
}), [props.onOpenSettings, props.settingsOpen, props.showSettingsButton]);
useControlAction(settingsControlAction);

return (
<div className="border-t border-dls-border bg-dls-surface">
Expand All @@ -143,6 +177,7 @@ export function StatusBar(props: StatusBarProps) {

<div className="flex items-center gap-1.5">
<button
ref={docsButtonRef}
type="button"
className="inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text"
onClick={() => platform.openLink(DOCS_URL)}
Expand All @@ -153,6 +188,7 @@ export function StatusBar(props: StatusBarProps) {
<span className="text-[11px] font-medium">{t("status.docs")}</span>
</button>
<button
ref={feedbackButtonRef}
type="button"
className="inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text"
onClick={props.onSendFeedback}
Expand All @@ -166,6 +202,7 @@ export function StatusBar(props: StatusBarProps) {
</button>
{props.showSettingsButton !== false ? (
<button
ref={settingsButtonRef}
type="button"
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text"
onClick={props.onOpenSettings}
Expand Down
Loading
Loading