Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a9bdb83
feat(servers): run Manager automation on linked servers
Oct 19, 2026
780d310
Merge remote-tracking branch 'origin/feat/linked-opencode-servers' in…
chriswritescode-dev Oct 19, 2026
9ec2ed5
fix(servers): address CodeRabbit review on linked-server automation
chriswritescode-dev Oct 19, 2026
7874afd
feat(servers): bring linked-server sessions to parity with local
chriswritescode-dev Oct 19, 2026
e4a4afb
fix(schedules): recover runs across reconnects and cancel on unavaila…
chriswritescode-dev Oct 19, 2026
cb9b764
fix(opencode-import): skip caches and worktrees when importing host s…
chriswritescode-dev Oct 19, 2026
36b8c1b
feat(servers): switch the sidebar listing between local and linked se…
chriswritescode-dev Oct 19, 2026
8277ec2
fix(terminal): remove terminals that exit cleanly
chriswritescode-dev Oct 19, 2026
96e22df
feat(servers): compact server switcher and mobile navigation for link…
chriswritescode-dev Oct 19, 2026
42dd733
fix(servers): open settings tabs with history and keep server selecti…
chriswritescode-dev Oct 19, 2026
2474559
Merge remote-tracking branch 'origin/feat/linked-opencode-servers' in…
chriswritescode-dev Oct 19, 2026
a926991
fix(navigation): ignore malformed server ids on remote project routes
chriswritescode-dev Oct 19, 2026
0a520ba
feat(models): share favorites, recents and variants across linked ser…
chriswritescode-dev Oct 19, 2026
c1e09f3
feat(servers): name the local server OC Manager in the switcher
chriswritescode-dev Oct 19, 2026
c18c358
feat(servers): show linked server projects on the home page and remov…
chriswritescode-dev Oct 19, 2026
8605dbf
feat(servers): open a remote directory session from a dialog
chriswritescode-dev Oct 19, 2026
360ceea
fix(servers): reconcile linked server statuses once the server list l…
Oct 19, 2026
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
144 changes: 144 additions & 0 deletions backend/src/db/migrations/202610181300-session-server-scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import type { Database } from 'bun:sqlite'
import { LOCAL_OPENCODE_SERVER_ID } from '@opencode-manager/shared/schemas'
import type { Migration } from '../migration-runner'

const SERVER_ID_COLUMN = 'server_id'
const PERMISSION_MODES_TABLE = 'session_permission_modes'
const PERMISSION_MODES_REBUILD_TABLE = 'session_permission_modes_rebuild'

interface ColumnInfo {
name: string
pk: number
}

function tableExists(db: Database, table: string): boolean {
return Boolean(
db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table),
)
}

function tableColumns(db: Database, table: string): ColumnInfo[] {
return db.prepare(`PRAGMA table_info(${table})`).all() as ColumnInfo[]
}

function hasColumn(db: Database, table: string, column: string): boolean {
return tableColumns(db, table).some((entry) => entry.name === column)
}

function primaryKeyColumns(db: Database, table: string): string[] {
return tableColumns(db, table)
.filter((column) => column.pk > 0)
.sort((left, right) => left.pk - right.pk)
.map((column) => column.name)
}

function addServerIdColumn(db: Database, table: string): void {
if (!tableExists(db, table) || hasColumn(db, table, SERVER_ID_COLUMN)) {
return
}
db.run(`ALTER TABLE ${table} ADD COLUMN server_id TEXT NOT NULL DEFAULT '${LOCAL_OPENCODE_SERVER_ID}'`)
}

function dropServerIdColumn(db: Database, table: string): void {
if (!tableExists(db, table) || !hasColumn(db, table, SERVER_ID_COLUMN)) {
return
}
db.run(`ALTER TABLE ${table} DROP COLUMN ${SERVER_ID_COLUMN}`)
}

function deleteRemoteRows(db: Database, table: string): void {
if (!tableExists(db, table) || !hasColumn(db, table, SERVER_ID_COLUMN)) {
return
}
db.run(`DELETE FROM ${table} WHERE ${SERVER_ID_COLUMN} <> '${LOCAL_OPENCODE_SERVER_ID}'`)
}

function rebuildPermissionModesForServers(db: Database): void {
if (!tableExists(db, PERMISSION_MODES_TABLE) || !hasColumn(db, PERMISSION_MODES_TABLE, SERVER_ID_COLUMN)) {
return
}
const primaryKey = primaryKeyColumns(db, PERMISSION_MODES_TABLE)
if (primaryKey.length === 2 && primaryKey[0] === SERVER_ID_COLUMN && primaryKey[1] === 'session_id') {
return
}

db.run(`DROP TABLE IF EXISTS ${PERMISSION_MODES_REBUILD_TABLE}`)
db.run(`
CREATE TABLE ${PERMISSION_MODES_REBUILD_TABLE} (
session_id TEXT NOT NULL,
server_id TEXT NOT NULL DEFAULT '${LOCAL_OPENCODE_SERVER_ID}',
mode TEXT NOT NULL CHECK(mode IN ('ask','auto')),
updated_at INTEGER NOT NULL,
PRIMARY KEY (server_id, session_id)
)
`)
db.run(`
INSERT INTO ${PERMISSION_MODES_REBUILD_TABLE}(session_id, server_id, mode, updated_at)
SELECT session_id, ${SERVER_ID_COLUMN}, mode, updated_at FROM ${PERMISSION_MODES_TABLE}
`)
db.run(`DROP TABLE ${PERMISSION_MODES_TABLE}`)
db.run(`ALTER TABLE ${PERMISSION_MODES_REBUILD_TABLE} RENAME TO ${PERMISSION_MODES_TABLE}`)
}

function rebuildPermissionModesForLocalSessions(db: Database): void {
if (!tableExists(db, PERMISSION_MODES_TABLE) || !hasColumn(db, PERMISSION_MODES_TABLE, SERVER_ID_COLUMN)) {
return
}
const primaryKey = primaryKeyColumns(db, PERMISSION_MODES_TABLE)
if (primaryKey.length === 1 && primaryKey[0] === 'session_id') {
return
}

db.run(`DROP TABLE IF EXISTS ${PERMISSION_MODES_REBUILD_TABLE}`)
db.run(`
CREATE TABLE ${PERMISSION_MODES_REBUILD_TABLE} (
session_id TEXT PRIMARY KEY,
mode TEXT NOT NULL CHECK(mode IN ('ask','auto')),
updated_at INTEGER NOT NULL
)
`)
db.run(`
INSERT INTO ${PERMISSION_MODES_REBUILD_TABLE}(session_id, mode, updated_at)
SELECT session_id, mode, updated_at FROM ${PERMISSION_MODES_TABLE}
WHERE ${SERVER_ID_COLUMN} = '${LOCAL_OPENCODE_SERVER_ID}'
`)
db.run(`DROP TABLE ${PERMISSION_MODES_TABLE}`)
db.run(`ALTER TABLE ${PERMISSION_MODES_REBUILD_TABLE} RENAME TO ${PERMISSION_MODES_TABLE}`)
}

const migration: Migration = {
id: '202610181300-session-server-scope',

up(db) {
addServerIdColumn(db, PERMISSION_MODES_TABLE)
addServerIdColumn(db, 'session_goals')
rebuildPermissionModesForServers(db)

db.run('DROP INDEX IF EXISTS idx_session_goals_open')
if (tableExists(db, 'session_goals')) {
db.run(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_session_goals_open
ON session_goals(server_id, session_id)
WHERE status IN ('active','paused')
`)
}
},

down(db) {
deleteRemoteRows(db, 'session_goals')
deleteRemoteRows(db, PERMISSION_MODES_TABLE)

db.run('DROP INDEX IF EXISTS idx_session_goals_open')
if (tableExists(db, 'session_goals')) {
db.run(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_session_goals_open
ON session_goals(session_id)
WHERE status IN ('active','paused')
`)
}
rebuildPermissionModesForLocalSessions(db)
dropServerIdColumn(db, 'session_goals')
},
}

export default migration
84 changes: 84 additions & 0 deletions backend/src/db/migrations/202610181310-multi-run-targets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { Migration } from '../migration-runner'

interface ColumnInfo {
name: string
notnull: number
}

const REBUILT_COLUMNS = `
id INTEGER PRIMARY KEY AUTOINCREMENT,
repo_id INTEGER,
server_id TEXT NOT NULL DEFAULT 'local',
directory TEXT,
name TEXT NOT NULL,
prompt TEXT NOT NULL,
isolated INTEGER NOT NULL,
base_ref TEXT,
created_at INTEGER NOT NULL
`

function needsRebuild(columns: ColumnInfo[]): boolean {
if (columns.length === 0) return false

const byName = new Map(columns.map((column) => [column.name, column]))
const repoId = byName.get('repo_id')
if (!repoId || repoId.notnull === 1) return true
return !byName.has('server_id') || !byName.has('directory')
}

const migration: Migration = {
id: '202610181310-multi-run-targets',

up(db) {
const columns = db.prepare('PRAGMA table_info(multi_runs)').all() as ColumnInfo[]
if (!needsRebuild(columns)) return

const names = new Set(columns.map((column) => column.name))
const serverId = names.has('server_id') ? 'server_id' : "'local'"
const directory = names.has('directory') ? 'directory' : 'NULL'

db.run('DROP TABLE IF EXISTS multi_runs_targets')
db.run(`CREATE TABLE multi_runs_targets (${REBUILT_COLUMNS})`)
db.run(`
INSERT INTO multi_runs_targets (id, repo_id, server_id, directory, name, prompt, isolated, base_ref, created_at)
SELECT id, repo_id, ${serverId}, ${directory}, name, prompt, isolated, base_ref, created_at
FROM multi_runs
`)
db.run('DROP TABLE multi_runs')
db.run('ALTER TABLE multi_runs_targets RENAME TO multi_runs')
db.run('CREATE INDEX IF NOT EXISTS idx_multi_runs_repo ON multi_runs(repo_id, created_at DESC)')
db.run('CREATE INDEX IF NOT EXISTS idx_multi_runs_server ON multi_runs(server_id, directory, created_at DESC)')
},

down(db) {
const columns = db.prepare('PRAGMA table_info(multi_runs)').all() as ColumnInfo[]
if (columns.length === 0) return

const names = new Set(columns.map((column) => column.name))
if (!names.has('server_id') && !names.has('directory')) return

db.run('DROP TABLE IF EXISTS multi_runs_repo_only')
db.run(`
CREATE TABLE multi_runs_repo_only (
id INTEGER PRIMARY KEY AUTOINCREMENT,
repo_id INTEGER NOT NULL,
name TEXT NOT NULL,
prompt TEXT NOT NULL,
isolated INTEGER NOT NULL,
base_ref TEXT,
created_at INTEGER NOT NULL
)
`)
db.run(`
INSERT INTO multi_runs_repo_only (id, repo_id, name, prompt, isolated, base_ref, created_at)
SELECT id, repo_id, name, prompt, isolated, base_ref, created_at
FROM multi_runs
WHERE repo_id IS NOT NULL
`)
db.run('DROP TABLE multi_runs')
db.run('ALTER TABLE multi_runs_repo_only RENAME TO multi_runs')
db.run('CREATE INDEX IF NOT EXISTS idx_multi_runs_repo ON multi_runs(repo_id, created_at DESC)')
},
}

export default migration
Loading