Skip to content
Open
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Design principles for hot reload:
* **Conservative triggers**: only reload when a file that OpenCode reads at startup actually changes inside `.opencode/` or `opencode.json`. Ignore metadata files like `openwork.json`, `.DS_Store`, etc.
* **Workspace-scoped**: reload state is keyed per workspace. Switching workspaces never leaks reload signals from one workspace to another.
* **Session-aware**: when sessions are actively running, queue reload signals. Promote to visible reload (toast or auto-reload) only after all active sessions finish. This avoids interrupting in-flight tool calls.
* **Auto-reload setting**: each workspace can opt into automatic reload via `.opencode/openwork.json` (`reload.auto`). When enabled, the engine reloads automatically once queued signals are ready and no sessions are active.
* **Auto-reload setting**: each workspace can opt into automatic reload via `.openwork/openwork.json` (`reload.auto`). When enabled, the engine reloads automatically once queued signals are ready and no sessions are active.
* **Session continuity**: before reload, capture running session IDs, agents, and models. After reload, optionally relaunch those sessions so the user experiences seamless continuity.
* **Per-workspace isolation**: the desktop file watcher only watches the runtime-connected workspace root and its `.opencode/` directory. This can differ briefly from the UI-selected workspace while the user browses another workspace. The server reload event store is already keyed by `workspaceId`.

Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Agents, skills, and commands should model the following as OpenWork server behav

- workspace creation and initialization
- writes to `.opencode/`, `opencode.json`, and `opencode.jsonc`
- OpenWork workspace config writes (`.opencode/openwork.json`)
- OpenWork workspace config writes (`.openwork/openwork.json`)
- workspace template export/import, including shareable `.opencode/**` files and `opencode.json` state
- workspace template starter-session materialization from portable blueprint config (not copied runtime session history)
- share-bundle publish/fetch flows used by OpenWork template links
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/i18n/locales/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1721,7 +1721,7 @@ export default {
"settings.window_appearance_desc": "Personalitza l'aspecte de la finestra.",
"settings.worker_id_label": "worker {id}",
"settings.worker_unresolved": "worker {runtimeWorkspaceId}",
"settings.workspace_config_desc": ".opencode/openwork.json",
"settings.workspace_config_desc": ".openwork/openwork.json",
"settings.workspace_config_title": "Configuració del workspace",
"settings.workspace_debug_events_label": "Esdeveniments de depuració del workspace",
"settings.workspace_fallback_name": "Workspace",
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1762,7 +1762,7 @@ export default {
"settings.window_appearance_desc": "Customize window appearance.",
"settings.worker_id_label": "Worker {id}",
"settings.worker_unresolved": "Worker {runtimeWorkspaceId}",
"settings.workspace_config_desc": ".opencode/openwork.json",
"settings.workspace_config_desc": ".openwork/openwork.json",
"settings.workspace_config_title": "Workspace config",
"settings.workspace_debug_events_label": "Workspace debug events",
"settings.workspace_fallback_name": "Workspace",
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/i18n/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1721,7 +1721,7 @@ export default {
"settings.window_appearance_desc": "Personaliza la apariencia de la ventana.",
"settings.worker_id_label": "Worker {id}",
"settings.worker_unresolved": "Worker {entorno de ejecuciónWorkspaceId}",
"settings.workspace_config_desc": ".opencode/openwork.json",
"settings.workspace_config_desc": ".openwork/openwork.json",
"settings.workspace_config_title": "Configuración del espacio de trabajo",
"settings.workspace_debug_events_label": "Eventos de depuración del espacio de trabajo",
"settings.workspace_fallback_name": "workspace",
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/i18n/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1721,7 +1721,7 @@ export default {
"settings.window_appearance_desc": "Personnalisez l'apparence de la fenêtre.",
"settings.worker_id_label": "Worker {id}",
"settings.worker_unresolved": "Worker {runtimeWorkspaceId}",
"settings.workspace_config_desc": ".opencode/openwork.json",
"settings.workspace_config_desc": ".openwork/openwork.json",
"settings.workspace_config_title": "Configuration de l'espace de travail",
"settings.workspace_debug_events_label": "Événements de débogage de l'espace de travail",
"settings.workspace_fallback_name": "Espace de travail",
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/i18n/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1703,7 +1703,7 @@ export default {
"settings.window_appearance_desc": "ウィンドウの外観をカスタマイズします。",
"settings.worker_id_label": "ワーカー{id}",
"settings.worker_unresolved": "ワーカー{runtimeWorkspaceId}",
"settings.workspace_config_desc": ".opencode/openwork.json",
"settings.workspace_config_desc": ".openwork/openwork.json",
"settings.workspace_config_title": "ワークスペース設定",
"settings.workspace_debug_events_label": "ワークスペースデバッグイベント",
"settings.workspace_fallback_name": "ワークスペース",
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/i18n/locales/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1704,7 +1704,7 @@ export default {
"settings.window_appearance_desc": "Personalizar aparência da janela.",
"settings.worker_id_label": "Worker {id}",
"settings.worker_unresolved": "Worker {runtimeWorkspaceId}",
"settings.workspace_config_desc": ".opencode/openwork.json",
"settings.workspace_config_desc": ".openwork/openwork.json",
"settings.workspace_config_title": "Config do workspace",
"settings.workspace_debug_events_label": "Eventos de depuração do workspace",
"settings.workspace_fallback_name": "Workspace",
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/i18n/locales/th.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1704,7 +1704,7 @@ export default {
"settings.window_appearance_desc": "ปรับแต่งรูปลักษณ์หน้าต่าง",
"settings.worker_id_label": "Worker {id}",
"settings.worker_unresolved": "Worker {runtimeWorkspaceId}",
"settings.workspace_config_desc": ".opencode/openwork.json",
"settings.workspace_config_desc": ".openwork/openwork.json",
"settings.workspace_config_title": "การตั้งค่าพื้นที่ทำงาน",
"settings.workspace_debug_events_label": "เหตุการณ์ดีบักพื้นที่ทำงาน",
"settings.workspace_fallback_name": "พื้นที่ทำงาน",
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/i18n/locales/vi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1704,7 +1704,7 @@ export default {
"settings.window_appearance_desc": "Tùy chỉnh giao diện cửa sổ.",
"settings.worker_id_label": "Worker {id}",
"settings.worker_unresolved": "Worker {runtimeWorkspaceId}",
"settings.workspace_config_desc": ".opencode/openwork.json",
"settings.workspace_config_desc": ".openwork/openwork.json",
"settings.workspace_config_title": "Cấu hình workspace",
"settings.workspace_debug_events_label": "Sự kiện gỡ lỗi workspace",
"settings.workspace_fallback_name": "Workspace",
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1707,7 +1707,7 @@ export default {
"settings.window_appearance_desc": "自定义窗口外观。",
"settings.worker_id_label": "工作区{id}",
"settings.worker_unresolved": "工作区{runtimeWorkspaceId}",
"settings.workspace_config_desc": ".opencode/openwork.json",
"settings.workspace_config_desc": ".openwork/openwork.json",
"settings.workspace_config_title": "工作区配置",
"settings.workspace_debug_events_label": "工作区调试事件",
"settings.workspace_fallback_name": "工作区",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ export function createProviderAuthStore(options: CreateProviderAuthStoreOptions)
});
if (!result.ok) {
throw new Error(
result.stderr || result.stdout || "Failed to write .opencode/openwork.json",
result.stderr || result.stdout || "Failed to write .openwork/openwork.json",
);
}
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export function createExtensionsStore(options: {
config: config as never,
});
if (!result.ok) {
throw new Error(result.stderr || result.stdout || "Failed to write .opencode/openwork.json");
throw new Error(result.stderr || result.stdout || "Failed to write .openwork/openwork.json");
}
return true;
}
Expand Down
20 changes: 16 additions & 4 deletions apps/desktop/electron/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -339,17 +339,29 @@ function defaultWorkspaceOpenworkConfig(workspacePath) {
};
}

function resolveOpenworkConfigReadPath(workspacePath) {
const newPath = path.join(workspacePath, ".openwork", "openwork.json");
if (existsSync(newPath)) return newPath;
const legacyPath = path.join(workspacePath, ".opencode", "openwork.json");
if (existsSync(legacyPath)) return legacyPath;
return newPath;
}

async function readWorkspaceOpenworkConfig(workspacePath) {
const openworkPath = path.join(workspacePath, ".opencode", "openwork.json");
const openworkPath = resolveOpenworkConfigReadPath(workspacePath);
if (!(await pathExists(openworkPath))) {
return defaultWorkspaceOpenworkConfig(workspacePath);
}
const raw = await readFile(openworkPath, "utf8");
return JSON.parse(raw);
try {
const raw = await readFile(openworkPath, "utf8");
return JSON.parse(raw);
} catch {
return defaultWorkspaceOpenworkConfig(workspacePath);
}
}

async function writeWorkspaceOpenworkConfig(workspacePath, config) {
const openworkPath = path.join(workspacePath, ".opencode", "openwork.json");
const openworkPath = path.join(workspacePath, ".openwork", "openwork.json");
await mkdir(path.dirname(openworkPath), { recursive: true });
await writeFile(openworkPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
return execResult(true, `Wrote ${openworkPath}`);
Expand Down
23 changes: 16 additions & 7 deletions apps/desktop/src-tauri/src/commands/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,9 @@ fn current_openwork_state_paths(app: &AppHandle) -> Result<Vec<PathBuf>, String>
];

if let Some(home) = home_dir() {
paths.push(
home.join("OpenWork")
.join("Welcome")
.join(".opencode")
.join("openwork.json"),
);
let welcome = home.join("OpenWork").join("Welcome");
let resolved = resolve_openwork_config_read_path(&welcome);
paths.push(resolved);
}

Ok(paths)
Expand Down Expand Up @@ -228,10 +225,22 @@ fn validate_server_name(name: &str) -> Result<String, String> {
Ok(trimmed.to_string())
}

fn resolve_openwork_config_read_path(workspace_root: &Path) -> PathBuf {
let new_path = workspace_root.join(".openwork").join("openwork.json");
if new_path.exists() {
return new_path;
}
let legacy_path = workspace_root.join(".opencode").join("openwork.json");
if legacy_path.exists() {
return legacy_path;
}
new_path
}

fn read_workspace_openwork_config(
workspace_path: &Path,
) -> Result<WorkspaceOpenworkConfig, String> {
let openwork_path = workspace_path.join(".opencode").join("openwork.json");
let openwork_path = resolve_openwork_config_read_path(workspace_path);
if !openwork_path.exists() {
let mut cfg = WorkspaceOpenworkConfig::default();
let workspace_value = workspace_path.to_string_lossy().to_string();
Expand Down
84 changes: 52 additions & 32 deletions apps/desktop/src-tauri/src/commands/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ use walkdir::WalkDir;
use zip::write::FileOptions;
use zip::{CompressionMethod, ZipArchive, ZipWriter};

fn resolve_openwork_config_read_path(workspace_root: &Path) -> PathBuf {
let new_path = workspace_root.join(".openwork").join("openwork.json");
if new_path.exists() {
return new_path;
}
let legacy_path = workspace_root.join(".opencode").join("openwork.json");
if legacy_path.exists() {
return legacy_path;
}
new_path
}

fn build_workspace_list(state: crate::types::WorkspaceState) -> WorkspaceList {
let watched_id = if state.watched_workspace_id.trim().is_empty() {
None
Expand Down Expand Up @@ -570,18 +582,13 @@ pub fn workspace_add_authorized_root(
return Err("folderPath is required".to_string());
}

let openwork_path = PathBuf::from(&workspace_path)
.join(".opencode")
.join("openwork.json");

if let Some(parent) = openwork_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create {}: {e}", parent.display()))?;
}
let ws = PathBuf::from(&workspace_path);
let openwork_read_path = resolve_openwork_config_read_path(&ws);
let openwork_write_path = ws.join(".openwork").join("openwork.json");

let mut config: WorkspaceOpenworkConfig = if openwork_path.exists() {
let raw = fs::read_to_string(&openwork_path)
.map_err(|e| format!("Failed to read {}: {e}", openwork_path.display()))?;
let mut config: WorkspaceOpenworkConfig = if openwork_read_path.exists() {
let raw = fs::read_to_string(&openwork_read_path)
.map_err(|e| format!("Failed to read {}: {e}", openwork_read_path.display()))?;
serde_json::from_str(&raw).unwrap_or_default()
} else {
let mut cfg = WorkspaceOpenworkConfig::default();
Expand All @@ -595,11 +602,16 @@ pub fn workspace_add_authorized_root(
config.authorized_roots.push(folder_path);
}

if let Some(parent) = openwork_write_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create {}: {e}", parent.display()))?;
}

fs::write(
&openwork_path,
&openwork_write_path,
serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?,
)
.map_err(|e| format!("Failed to write {}: {e}", openwork_path.display()))?;
.map_err(|e| format!("Failed to write {}: {e}", openwork_write_path.display()))?;

Ok(ExecResult {
ok: true,
Expand All @@ -619,9 +631,8 @@ pub fn workspace_openwork_read(
return Err("workspacePath is required".to_string());
}

let openwork_path = PathBuf::from(&workspace_path)
.join(".opencode")
.join("openwork.json");
let ws = PathBuf::from(&workspace_path);
let openwork_path = resolve_openwork_config_read_path(&ws);

if !openwork_path.exists() {
let mut cfg = WorkspaceOpenworkConfig::default();
Expand All @@ -648,7 +659,7 @@ pub fn workspace_openwork_write(
}

let openwork_path = PathBuf::from(&workspace_path)
.join(".opencode")
.join(".openwork")
.join("openwork.json");

if let Some(parent) = openwork_path.parent() {
Expand Down Expand Up @@ -730,9 +741,12 @@ fn collect_workspace_entries(
}
}

let opencode_dir = workspace_root.join(".opencode");
if opencode_dir.exists() {
for entry in WalkDir::new(&opencode_dir) {
for dir_name in [".opencode", ".openwork"] {
let dir = workspace_root.join(dir_name);
if !dir.exists() {
continue;
}
for entry in WalkDir::new(&dir) {
let entry = entry.map_err(|e| e.to_string())?;
if !entry.file_type().is_file() {
continue;
Expand Down Expand Up @@ -897,7 +911,7 @@ pub fn workspace_import_config(
}) {
return Err("Archive contains an unsafe path".to_string());
}
if !(name == "opencode.json" || name.starts_with(".opencode/")) {
if !(name == "opencode.json" || name.starts_with(".opencode/") || name.starts_with(".openwork/")) {
continue;
}
if let Some(file_name) = entry_path.file_name().and_then(|entry| entry.to_str()) {
Expand All @@ -924,17 +938,19 @@ pub fn workspace_import_config(
}

let opencode_dir = target_path.join(".opencode");
if !opencode_dir.exists() {
return Err("Archive is missing .opencode config".to_string());
let openwork_dir = target_path.join(".openwork");
if !opencode_dir.exists() && !openwork_dir.exists() {
return Err("Archive is missing workspace config".to_string());
}

let openwork_path = target_path.join(".opencode").join("openwork.json");
let openwork_read_path = resolve_openwork_config_read_path(&target_path);
let openwork_write_path = target_path.join(".openwork").join("openwork.json");
let mut preset = "starter".to_string();
let mut workspace_name = name.clone().filter(|value| !value.trim().is_empty());

if openwork_path.exists() {
let raw = fs::read_to_string(&openwork_path)
.map_err(|e| format!("Failed to read {}: {e}", openwork_path.display()))?;
if openwork_read_path.exists() {
let raw = fs::read_to_string(&openwork_read_path)
.map_err(|e| format!("Failed to read {}: {e}", openwork_read_path.display()))?;
if let Ok(mut config) = serde_json::from_str::<WorkspaceOpenworkConfig>(&raw) {
config.authorized_roots = vec![target_dir.clone()];
if let Some(workspace) = &config.workspace {
Expand All @@ -950,23 +966,27 @@ pub fn workspace_import_config(
}
}
}
if let Some(parent) = openwork_write_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create {}: {e}", parent.display()))?;
}
fs::write(
&openwork_path,
&openwork_write_path,
serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?,
)
.map_err(|e| format!("Failed to write {}: {e}", openwork_path.display()))?;
.map_err(|e| format!("Failed to write {}: {e}", openwork_write_path.display()))?;
}
} else {
let config = WorkspaceOpenworkConfig::new(&target_dir, &preset, now_ms());
if let Some(parent) = openwork_path.parent() {
if let Some(parent) = openwork_write_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create {}: {e}", parent.display()))?;
}
fs::write(
&openwork_path,
&openwork_write_path,
serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?,
)
.map_err(|e| format!("Failed to write {}: {e}", openwork_path.display()))?;
.map_err(|e| format!("Failed to write {}: {e}", openwork_write_path.display()))?;
}

let name = workspace_name
Expand Down
13 changes: 7 additions & 6 deletions apps/desktop/src-tauri/src/workspace/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,18 +537,19 @@ pub fn ensure_workspace_files(workspace_path: &str, preset: &str) -> Result<(),
.map_err(|e| format!("Failed to write {}: {e}", config_path.display()))?;
}

let openwork_path = root.join(".opencode").join("openwork.json");
if !openwork_path.exists() {
let openwork_new_path = root.join(".openwork").join("openwork.json");
let openwork_legacy_path = root.join(".opencode").join("openwork.json");
if !openwork_new_path.exists() && !openwork_legacy_path.exists() {
let openwork = WorkspaceOpenworkConfig::new(workspace_path, preset, now_ms());

fs::create_dir_all(openwork_path.parent().unwrap())
.map_err(|e| format!("Failed to create {}: {e}", openwork_path.display()))?;
fs::create_dir_all(openwork_new_path.parent().unwrap())
.map_err(|e| format!("Failed to create {}: {e}", openwork_new_path.display()))?;

fs::write(
&openwork_path,
&openwork_new_path,
serde_json::to_string_pretty(&openwork).map_err(|e| e.to_string())?,
)
.map_err(|e| format!("Failed to write {}: {e}", openwork_path.display()))?;
.map_err(|e| format!("Failed to write {}: {e}", openwork_new_path.display()))?;
}

Ok(())
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/src-tauri/src/workspace/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ pub fn update_workspace_watch(
.map_err(|e| format!("Failed to watch .opencode: {e}"))?;
}

let openwork_dir = root.join(".openwork");
if openwork_dir.exists() {
watcher
.watch(&openwork_dir, RecursiveMode::Recursive)
.map_err(|e| format!("Failed to watch .openwork: {e}"))?;
}

*state
.root
.lock()
Expand Down
Loading