Prepare v0.1.0 release

This commit is contained in:
Benjamin Shafii
2026-01-13 18:40:23 -08:00
parent 39273227f3
commit aeab2ec71a
6 changed files with 115 additions and 54 deletions

View File

@@ -1,96 +1,103 @@
# OpenWork
OpenWork is a native (Tauri) desktop UI for running **OpenCode** with a friendly, non-technical workflow.
OpenWork is an **extensible, open-source “Claude Work” style system for knowledge workers**.
It supports:
- **Host mode**: start `opencode serve` locally in a chosen project folder.
Its a native desktop app (Tauri) that runs **OpenCode** under the hood, but presents it as a clean, guided workflow:
- pick a workspace
- start a run
- watch progress + plan updates
- approve permissions when needed
- reuse what works (templates + skills)
The goal: make “agentic work” feel like a product, not a terminal.
## Why
Knowledge workers dont want to learn a CLI, fight config sprawl, or rebuild the same workflows in every repo.
OpenWork is designed to be:
- **Extensible**: skills and workflows are installable modules.
- **Auditable**: show what happened, when, and why.
- **Permissioned**: explicit user approval for risky actions.
- **Portable**: keep logic in prompts/skills, not bespoke code.
## Whats Included (v0.1)
- **Host mode**: start `opencode serve` locally in a chosen folder.
- **Client mode**: connect to an existing OpenCode server by URL.
- **Sessions**: create/select sessions, send prompts, stream live updates via SSE.
- **Sessions**: create/select sessions and send prompts.
- **Live streaming**: SSE `/event` subscription for realtime updates.
- **Execution plan**: render OpenCode todos as a timeline.
- **Permissions**: surface permission requests and reply (allow once / always / deny).
- **Templates**: save and re-run common workflows (stored locally).
- **Skills manager**: view installed skills and install/import skills via OpenPackage.
- **Skills manager**:
- list installed `.opencode/skill` folders
- install from OpenPackage (`opkg install ...`)
- import a local skill folder into `.opencode/skill/<skill-name>`
## Requirements
## Quick Start
### Requirements
- Node.js + `pnpm`
- Rust toolchain (for Tauri): `cargo`, `rustc`
- OpenCode CLI installed and available on PATH: `opencode`
## Quick Start
Install dependencies:
### Install
```bash
pnpm install
```
Run as a desktop app (Tauri):
### Run (Desktop)
```bash
pnpm dev
```
Run the web UI only:
### Run (Web UI only)
```bash
pnpm dev:web
```
## How It Works
## Architecture (high-level)
- In **Host mode**, OpenWork spawns:
- `opencode serve --hostname 127.0.0.1 --port <free-port>`
- with the selected project folder as the process working directory.
- with your selected project folder as the process working directory.
- The UI uses `@opencode-ai/sdk/v2/client` to:
- connect to the server
- list/create sessions
- send prompts
- subscribe to `/event` SSE
- subscribe to SSE events
- read todos and permission requests
## Folder Picker
The folder picker uses the Tauri dialog plugin. The capability permission file is at:
The folder picker uses the Tauri dialog plugin.
Capability permissions are defined in:
- `src-tauri/capabilities/default.json`
## Skills (OpenPackage)
## OpenPackage Notes
The **Skills** screen supports:
- Installing a package (e.g. `github:anthropics/claude-code`) into the current workspace.
- Importing a local skill folder into `.opencode/skill/<skill-name>`.
If `opkg` is not installed globally, OpenWork falls back to:
Notes:
- If `opkg` is not installed globally, OpenWork falls back to `pnpm dlx opkg ...`.
```bash
pnpm dlx opkg install <package>
```
## Useful Scripts
Typecheck:
## Useful Commands
```bash
pnpm typecheck
```
Build web bundle:
```bash
pnpm build:web
```
API smoke tests (non-UI):
```bash
pnpm test:e2e
pnpm test:events
pnpm test:sessions
pnpm test:health
```
## Security Notes
- OpenWork intentionally hides model reasoning/tool metadata by default.
- Permissions are surfaced via OpenCode permission requests (when configured to ask).
- Host mode is local by default (`127.0.0.1`).
- OpenWork hides model reasoning and sensitive tool metadata by default.
- Host mode binds to `127.0.0.1` by default.
## License

View File

@@ -1,7 +1,7 @@
{
"name": "@different-ai/openwork",
"private": true,
"version": "0.0.0",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "tauri dev",

2
src-tauri/Cargo.lock generated
View File

@@ -2014,7 +2014,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openwork"
version = "0.0.0"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",

View File

@@ -1,6 +1,6 @@
[package]
name = "openwork"
version = "0.0.0"
version = "0.1.0"
description = "OpenWork"
authors = ["Different AI"]
edition = "2021"

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenWork",
"version": "0.0.0",
"version": "0.1.0",
"identifier": "com.differentai.openwork",
"build": {
"beforeDevCommand": "pnpm dev:web",

View File

@@ -290,8 +290,29 @@ export default function App() {
const [developerMode, setDeveloperMode] = createSignal(false);
const [busy, setBusy] = createSignal(false);
const [busyLabel, setBusyLabel] = createSignal<string | null>(null);
const [busyStartedAt, setBusyStartedAt] = createSignal<number | null>(null);
const [error, setError] = createSignal<string | null>(null);
const busySeconds = createMemo(() => {
const start = busyStartedAt();
if (!start) return 0;
return Math.max(0, Math.round((Date.now() - start) / 1000));
});
const newTaskDisabled = createMemo(() => {
const label = busyLabel();
// Allow creating a new session even while a run is in progress.
if (busy() && label === "Running") return false;
// Otherwise, block during engine / connection transitions.
if (busy() && (label === "Connecting" || label === "Starting engine" || label === "Disconnecting")) {
return true;
}
return busy();
});
const selectedSession = createMemo(() => {
const id = selectedSessionId();
if (!id) return null;
@@ -351,6 +372,8 @@ export default function App() {
async function connectToServer(nextBaseUrl: string, directory?: string) {
setError(null);
setBusy(true);
setBusyLabel("Connecting");
setBusyStartedAt(Date.now());
setSseConnected(false);
try {
@@ -375,10 +398,12 @@ export default function App() {
} catch (e) {
setClient(null);
setConnectedVersion(null);
setError(e instanceof Error ? e.message : "Unknown error");
setError(e instanceof Error ? e.message : safeStringify(e));
return false;
} finally {
setBusy(false);
setBusyLabel(null);
setBusyStartedAt(null);
}
}
@@ -396,6 +421,8 @@ export default function App() {
setError(null);
setBusy(true);
setBusyLabel("Starting engine");
setBusyStartedAt(Date.now());
try {
const info = await engineStart(dir);
@@ -407,15 +434,21 @@ export default function App() {
}
return true;
} catch (e) {
setError(e instanceof Error ? e.message : safeStringify(e));
}
} catch (e) {
setError(e instanceof Error ? e.message : safeStringify(e));
return false;
} finally {
setBusy(false);
setBusyLabel(null);
setBusyStartedAt(null);
}
}
async function stopHost() {
setError(null);
setBusy(true);
setBusyLabel("Disconnecting");
setBusyStartedAt(Date.now());
try {
if (isTauriRuntime()) {
@@ -437,9 +470,11 @@ export default function App() {
setOnboardingStep("mode");
setView("onboarding");
} catch (e) {
setError(e instanceof Error ? e.message : "Unknown error");
setError(e instanceof Error ? e.message : safeStringify(e));
} finally {
setBusy(false);
setBusyLabel(null);
setBusyStartedAt(null);
}
}
@@ -471,6 +506,8 @@ export default function App() {
if (!c) return;
setBusy(true);
setBusyLabel("Creating session");
setBusyStartedAt(Date.now());
setError(null);
try {
@@ -479,9 +516,11 @@ export default function App() {
await selectSession(session.id);
setView("session");
} catch (e) {
setError(e instanceof Error ? e.message : "Unknown error");
setError(e instanceof Error ? e.message : safeStringify(e));
} finally {
setBusy(false);
setBusyLabel(null);
setBusyStartedAt(null);
}
}
@@ -494,6 +533,8 @@ export default function App() {
if (!content) return;
setBusy(true);
setBusyLabel("Running");
setBusyStartedAt(Date.now());
setError(null);
try {
@@ -517,9 +558,11 @@ export default function App() {
await loadSessions(c);
} catch (e) {
setError(e instanceof Error ? e.message : "Unknown error");
setError(e instanceof Error ? e.message : safeStringify(e));
} finally {
setBusy(false);
setBusyLabel(null);
setBusyStartedAt(null);
}
}
@@ -998,6 +1041,12 @@ export default function App() {
return bits.join(" · ");
});
const busyHint = createMemo(() => {
if (!busy() || !busyLabel()) return null;
const seconds = busySeconds();
return seconds > 0 ? `${busyLabel()} · ${seconds}s` : busyLabel();
});
function OnboardingView() {
return (
<Switch>
@@ -1406,6 +1455,7 @@ export default function App() {
);
};
const content = () => (
<Switch>
<Match when={tab() === "home"}>
@@ -1420,7 +1470,8 @@ export default function App() {
</div>
<Button
onClick={createSessionAndOpen}
disabled={busy()}
disabled={newTaskDisabled()}
title={newTaskDisabled() ? busyHint() ?? "Busy" : ""}
class="w-full md:w-auto py-3 px-6 text-base"
>
<Play size={18} />
@@ -1816,10 +1867,13 @@ export default function App() {
</div>
<h1 class="text-lg font-medium">{title()}</h1>
<span class="text-xs text-zinc-600">{headerStatus()}</span>
<Show when={busyHint()}>
<span class="text-xs text-zinc-500">· {busyHint()}</span>
</Show>
</div>
<div class="flex items-center gap-2">
<Show when={tab() === "home" || tab() === "sessions"}>
<Button onClick={createSessionAndOpen} disabled={busy()}>
<Button onClick={createSessionAndOpen} disabled={newTaskDisabled()} title={newTaskDisabled() ? busyHint() ?? "Busy" : ""}>
<Play size={16} />
New Task
</Button>