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
15 changes: 15 additions & 0 deletions .opencode/skills/openwork-docker-chrome-mcp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ Evidence:
- Take a Chrome MCP screenshot after the response appears.
- If something fails, capture console logs and (optionally) Docker logs.

### Verification checklist (copy into PR)

- [ ] Started stack with `packaging/docker/dev-up.sh` from repo root.
- [ ] Used the printed Web UI URL (not a guessed port).
- [ ] Completed one full user flow in the UI (input -> action -> visible result).
- [ ] Captured at least one screenshot for the success state.
- [ ] Captured failure evidence when relevant (console and/or Docker logs).
- [ ] Stopped stack with the exact printed `docker compose -p ... down` command.

Suggested screenshot set for user-facing changes:
- Before action state.
- During action/progress state.
- Success state.
- Failure or recovery state (if applicable).

### 3) Stop the stack

Use the exact `docker compose -p ... down` command printed by `dev-up.sh`.
Expand Down
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ Read INFRASTRUCTURE.md
* **Slick and fluid**: 60fps animations, micro-interactions, premium feel.
* **Mobile-native**: touch targets, gestures, and layouts optimized for small screens.

## Task Intake (Required)

Before making changes, explicitly confirm the target repository in your first task update.

Required format:

1. `Target repo: <path>` (for example: `_repos/openwork`)
2. `Out of scope repos: <list>` (for example: `_repos/opencode`)
3. `Planned output: <what will be changed/tested>`

If the user request references multiple repos and the intended edit location is ambiguous, stop after discovery and ask for a single repo target before editing files.

## New Feature Workflow (Required)

When the user asks to create a new feature, follow this exact procedure:
Expand Down
8 changes: 8 additions & 0 deletions packages/app/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4600,6 +4600,9 @@ export default function App() {
engineDoctorCheckedAt: engineDoctorCheckedAt(),
engineInstallLogs: engineInstallLogs(),
error: error(),
canRepairMigration: workspaceStore.canRepairOpencodeMigration(),
migrationRepairBusy: workspaceStore.migrationRepairBusy(),
migrationRepairResult: workspaceStore.migrationRepairResult(),
isWindows: isWindowsPlatform(),
onClientDirectoryChange: setClientDirectory,
onOpenworkHostUrlChange: (value: string) =>
Expand All @@ -4615,6 +4618,7 @@ export default function App() {
onSelectStartup: workspaceStore.onSelectStartup,
onRememberStartupToggle: workspaceStore.onRememberStartupToggle,
onStartHost: workspaceStore.onStartHost,
onRepairMigration: workspaceStore.onRepairOpencodeMigration,
onCreateWorkspace: workspaceStore.createWorkspaceFlow,
onPickWorkspaceFolder: workspaceStore.pickWorkspaceFolder,
onImportWorkspaceConfig: workspaceStore.importWorkspaceConfig,
Expand Down Expand Up @@ -4859,6 +4863,10 @@ export default function App() {
workspaceDebugEvents: workspaceStore.workspaceDebugEvents(),
clearWorkspaceDebugEvents: workspaceStore.clearWorkspaceDebugEvents,
safeStringify,
repairOpencodeMigration: workspaceStore.repairOpencodeMigration,
migrationRepairBusy: workspaceStore.migrationRepairBusy(),
migrationRepairResult: workspaceStore.migrationRepairResult(),
migrationRepairAvailable: workspaceStore.canRepairOpencodeMigration(),
repairOpencodeCache,
cacheRepairBusy: cacheRepairBusy(),
cacheRepairResult: cacheRepairResult(),
Expand Down
156 changes: 154 additions & 2 deletions packages/app/src/app/context/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { downloadDir, homeDir } from "@tauri-apps/api/path";
import {
engineDoctor,
engineInfo,
opencodeDbMigrate,
engineInstall,
engineStart,
engineStop,
Expand Down Expand Up @@ -94,6 +95,11 @@ export type SandboxCreateProgressState = {

export type SandboxCreatePhase = "idle" | "preflight" | "provisioning" | "finalizing";

export type MigrationRepairResult = {
ok: boolean;
message: string;
};

export function createWorkspaceStore(options: {
startupPreference: () => StartupPreference | null;
setStartupPreference: (value: StartupPreference | null) => void;
Expand Down Expand Up @@ -177,6 +183,15 @@ export function createWorkspaceStore(options: {

const connectInFlightByKey = new Map<string, Promise<boolean>>();
let createRemoteInFlight: Promise<boolean> | null = null;
const DEFAULT_CONNECT_HEALTH_TIMEOUT_MS = 12_000;
const LOCAL_BOOT_CONNECT_HEALTH_TIMEOUT_MS = 180_000;
const LONG_BOOT_CONNECT_REASONS = new Set(["host-start", "bootstrap-local"]);
const DB_MIGRATE_UNSUPPORTED_PATTERNS = [
/unknown(?:\s+sub)?command\s+['"`]?db['"`]?/i,
/unrecognized(?:\s+sub)?command\s+['"`]?db['"`]?/i,
/no such command[:\s]+db/i,
/found argument ['"`]db['"`] which wasn't expected/i,
] as const;

const connectRequestKey = (
nextBaseUrl: string,
Expand All @@ -202,6 +217,26 @@ export function createWorkspaceStore(options: {
String(connectOptions?.navigate ?? true),
].join("::");

const resolveConnectHealthTimeoutMs = (reason?: string) => {
const normalizedReason = reason?.trim() ?? "";
if (LONG_BOOT_CONNECT_REASONS.has(normalizedReason)) {
return LOCAL_BOOT_CONNECT_HEALTH_TIMEOUT_MS;
}
return DEFAULT_CONNECT_HEALTH_TIMEOUT_MS;
};

const formatExecOutput = (result: { stdout: string; stderr: string }) => {
const stderr = result.stderr.trim();
const stdout = result.stdout.trim();
return [stderr, stdout].filter(Boolean).join("\n\n");
};

const isDbMigrateUnsupported = (output: string) => {
const normalized = output.trim();
if (!normalized) return false;
return DB_MIGRATE_UNSUPPORTED_PATTERNS.some((pattern) => pattern.test(normalized));
};

const [engine, setEngine] = createSignal<EngineInfo | null>(null);
const [engineAuth, setEngineAuth] = createSignal<OpencodeAuth | null>(null);
const [engineDoctorResult, setEngineDoctorResult] = createSignal<EngineDoctorResult | null>(null);
Expand Down Expand Up @@ -280,6 +315,8 @@ export function createWorkspaceStore(options: {
>({});
const [exportingWorkspaceConfig, setExportingWorkspaceConfig] = createSignal(false);
const [importingWorkspaceConfig, setImportingWorkspaceConfig] = createSignal(false);
const [migrationRepairBusy, setMigrationRepairBusy] = createSignal(false);
const [migrationRepairResult, setMigrationRepairResult] = createSignal<MigrationRepairResult | null>(null);

const activeWorkspaceInfo = createMemo(() => workspaces().find((w) => w.id === activeWorkspaceId()) ?? null);
const activeWorkspaceDisplay = createMemo<WorkspaceDisplay>(() => {
Expand Down Expand Up @@ -1153,6 +1190,7 @@ export function createWorkspaceStore(options: {
reason: context?.reason ?? null,
workspaceType: context?.workspaceType ?? null,
targetRoot: context?.targetRoot ?? null,
healthTimeoutMs: resolveConnectHealthTimeoutMs(context?.reason),
quiet: connectOptions?.quiet ?? false,
navigate: connectOptions?.navigate ?? true,
authMode: auth && "mode" in auth ? (auth as any).mode : auth ? "basic" : "none",
Expand Down Expand Up @@ -1182,9 +1220,14 @@ export function createWorkspaceStore(options: {
try {
let resolvedDirectory = directory?.trim() ?? "";
let nextClient = createClient(nextBaseUrl, resolvedDirectory || undefined, auth);
const health = await waitForHealthy(nextClient, { timeoutMs: 12_000 });
const healthTimeoutMs = resolveConnectHealthTimeoutMs(context?.reason);
const health = await waitForHealthy(nextClient, { timeoutMs: healthTimeoutMs });
connectMetrics.healthyMs = Date.now() - connectStart;
wsDebug("connect:healthy", { ms: Date.now() - connectStart, version: health.version });
wsDebug("connect:healthy", {
ms: Date.now() - connectStart,
version: health.version,
timeoutMs: healthTimeoutMs,
});

if (context?.workspaceType === "remote" && !resolvedDirectory) {
try {
Expand Down Expand Up @@ -2208,6 +2251,109 @@ export function createWorkspaceStore(options: {
}
}

function canRepairOpencodeMigration() {
if (!isTauriRuntime()) return false;
const workspace = activeWorkspaceInfo();
if (!workspace || workspace.workspaceType !== "local") return false;
return Boolean(activeWorkspacePath().trim());
}

async function repairOpencodeMigration(optionsOverride?: { navigate?: boolean }) {
if (!isTauriRuntime()) {
const message = t("app.migration.desktop_required", currentLocale());
setMigrationRepairResult({ ok: false, message });
options.setError(message);
return false;
}

if (migrationRepairBusy()) return false;

const workspace = activeWorkspaceInfo();
if (!workspace || workspace.workspaceType !== "local") {
const message = t("app.migration.local_only", currentLocale());
setMigrationRepairResult({ ok: false, message });
options.setError(message);
return false;
}

const root = activeWorkspacePath().trim();
if (!root) {
const message = t("app.migration.workspace_required", currentLocale());
setMigrationRepairResult({ ok: false, message });
options.setError(message);
return false;
}

setMigrationRepairBusy(true);
setMigrationRepairResult(null);
options.setError(null);
options.setBusy(true);
options.setBusyLabel("status.repairing_migration");
options.setBusyStartedAt(Date.now());

try {
if (engine()?.running) {
const info = await engineStop();
setEngine(info);
}

const source = options.engineSource();
const result = await opencodeDbMigrate({
projectDir: root,
preferSidecar: source === "sidecar",
opencodeBinPath: source === "custom" ? options.engineCustomBinPath?.().trim() || null : null,
});

if (!result.ok) {
const output = formatExecOutput(result);
if (isDbMigrateUnsupported(output)) {
const message = t("app.migration.unsupported", currentLocale());
setMigrationRepairResult({ ok: false, message });
options.setError(message);
return false;
}

const fallback = t("app.migration.failed", currentLocale());
const message = output ? `${fallback}\n\n${output}` : fallback;
setMigrationRepairResult({ ok: false, message });
options.setError(addOpencodeCacheHint(message));
return false;
}

const started = await startHost({
workspacePath: root,
navigate: optionsOverride?.navigate ?? false,
});
if (!started) {
const message = t("app.migration.restart_failed", currentLocale());
setMigrationRepairResult({ ok: false, message });
return false;
}

setMigrationRepairResult({ ok: true, message: t("app.migration.success", currentLocale()) });
return true;
} catch (error) {
const message = addOpencodeCacheHint(error instanceof Error ? error.message : safeStringify(error));
setMigrationRepairResult({ ok: false, message });
options.setError(message);
return false;
} finally {
setMigrationRepairBusy(false);
options.setBusy(false);
options.setBusyLabel(null);
options.setBusyStartedAt(null);
}
}

async function onRepairOpencodeMigration() {
options.setStartupPreference("local");
options.setOnboardingStep("connecting");
const ok = await repairOpencodeMigration({ navigate: true });
if (!ok) {
options.setOnboardingStep("local");
}
}

async function startHost(optionsOverride?: { workspacePath?: string; navigate?: boolean }) {
if (!isTauriRuntime()) {
options.setError(t("app.error.tauri_required", currentLocale()));
Expand Down Expand Up @@ -2258,6 +2404,7 @@ export function createWorkspaceStore(options: {
}

options.setError(null);
setMigrationRepairResult(null);
options.setBusy(true);
options.setBusyLabel("status.starting_engine");
options.setBusyStartedAt(Date.now());
Expand Down Expand Up @@ -2856,6 +3003,8 @@ export function createWorkspaceStore(options: {
workspaceConnectionStateById,
exportingWorkspaceConfig,
importingWorkspaceConfig,
migrationRepairBusy,
migrationRepairResult,
activeWorkspaceDisplay,
activeWorkspacePath,
activeWorkspaceRoot,
Expand Down Expand Up @@ -2883,13 +3032,16 @@ export function createWorkspaceStore(options: {
pickWorkspaceFolder,
exportWorkspaceConfig,
importWorkspaceConfig,
canRepairOpencodeMigration,
repairOpencodeMigration,
startHost,
stopHost,
reloadWorkspaceEngine,
bootstrapOnboarding,
onSelectStartup,
onBackToWelcome,
onStartHost,
onRepairOpencodeMigration,
onAttachHost,
onConnectClient,
onRememberStartupToggle,
Expand Down
17 changes: 17 additions & 0 deletions packages/app/src/app/lib/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,23 @@ export async function setOpenCodeRouterGroupsEnabled(enabled: boolean): Promise<
}
}

export async function opencodeDbMigrate(input: {
projectDir: string;
preferSidecar?: boolean;
opencodeBinPath?: string | null;
}): Promise<ExecResult> {
const safeProjectDir = input.projectDir.trim();
if (!safeProjectDir) {
throw new Error("project_dir is required");
}

return invoke<ExecResult>("opencode_db_migrate", {
projectDir: safeProjectDir,
preferSidecar: input.preferSidecar ?? false,
opencodeBinPath: input.opencodeBinPath ?? null,
});
}

export async function opencodeMcpAuth(
projectDir: string,
serverName: string,
Expand Down
8 changes: 8 additions & 0 deletions packages/app/src/app/pages/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ export type DashboardViewProps = {
workspaceDebugEvents: unknown;
clearWorkspaceDebugEvents: () => void;
safeStringify: (value: unknown) => string;
repairOpencodeMigration: () => void;
migrationRepairBusy: boolean;
migrationRepairResult: { ok: boolean; message: string } | null;
migrationRepairAvailable: boolean;
repairOpencodeCache: () => void;
cacheRepairBusy: boolean;
cacheRepairResult: string | null;
Expand Down Expand Up @@ -1428,6 +1432,10 @@ export default function DashboardView(props: DashboardViewProps) {
workspaceDebugEvents={props.workspaceDebugEvents}
clearWorkspaceDebugEvents={props.clearWorkspaceDebugEvents}
safeStringify={props.safeStringify}
repairOpencodeMigration={props.repairOpencodeMigration}
migrationRepairBusy={props.migrationRepairBusy}
migrationRepairResult={props.migrationRepairResult}
migrationRepairAvailable={props.migrationRepairAvailable}
repairOpencodeCache={props.repairOpencodeCache}
cacheRepairBusy={props.cacheRepairBusy}
cacheRepairResult={props.cacheRepairResult}
Expand Down
Loading
Loading