feat(electrobun): groups, cloning, shortcuts, custom window — all 5 features

Groups Sidebar:
- SQLite groups table (4 seeded: Development, Testing, DevOps, Research)
- Left icon rail with emoji group icons, Ctrl+1-4 switching
- Active group highlighted, projects filtered by group

Project Cloning:
- Clone button on project cards (fork icon)
- git worktree add via Bun.spawn (array form, no shell strings)
- 3-clone limit, branch name validation, pending-status pattern
- Clone cards: WT badge + branch name + accent top border
- Chain link SVG icons between linked clones in grid

Keyboard Shortcuts:
- keybinding-store.svelte.ts: 16 defaults across 4 categories
- Two-scope: document capture + terminal focus guard
- KeyboardSettings.svelte: search, click-to-capture, conflict detection
- Per-binding reset + Reset All

Custom Window:
- titleBarStyle: "hidden" — no native title bar
- Vertical "AGOR" text in left sidebar (writing-mode: vertical-rl)
- Floating window controls badge (minimize/maximize/close)
- Draggable region via -webkit-app-region: drag
- Window frame persisted to SQLite (debounced 500ms)

Window is resizable by default (Electrobun BrowserWindow).
This commit is contained in:
Hibryda 2026-03-20 06:24:24 +01:00
parent 5032021915
commit a020f59cb4
14 changed files with 1741 additions and 189 deletions

View file

@ -231891,6 +231891,24 @@ CREATE TABLE IF NOT EXISTS custom_themes (
name TEXT NOT NULL,
palette TEXT NOT NULL -- JSON blob
);
CREATE TABLE IF NOT EXISTS groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
icon TEXT NOT NULL,
position INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS keybindings (
id TEXT PRIMARY KEY,
chord TEXT NOT NULL
);
`;
var SEED_GROUPS = `
INSERT OR IGNORE INTO groups VALUES ('dev', 'Development', '\uD83D\uDD27', 0);
INSERT OR IGNORE INTO groups VALUES ('test', 'Testing', '\uD83E\uDDEA', 1);
INSERT OR IGNORE INTO groups VALUES ('ops', 'DevOps', '\uD83D\uDE80', 2);
INSERT OR IGNORE INTO groups VALUES ('research', 'Research', '\uD83D\uDD2C', 3);
`;
class SettingsDb {
@ -231899,6 +231917,7 @@ class SettingsDb {
mkdirSync2(CONFIG_DIR, { recursive: true });
this.db = new Database2(DB_PATH);
this.db.exec(SCHEMA);
this.db.exec(SEED_GROUPS);
const version = this.db.query("SELECT version FROM schema_version LIMIT 1").get();
if (!version) {
this.db.exec("INSERT INTO schema_version (version) VALUES (1)");
@ -231940,6 +231959,9 @@ class SettingsDb {
}
});
}
listGroups() {
return this.db.query("SELECT id, name, icon, position FROM groups ORDER BY position ASC").all();
}
getCustomThemes() {
const rows = this.db.query("SELECT id, name, palette FROM custom_themes").all();
return rows.flatMap((r) => {
@ -231957,6 +231979,16 @@ class SettingsDb {
deleteCustomTheme(id) {
this.db.query("DELETE FROM custom_themes WHERE id = ?").run(id);
}
getKeybindings() {
const rows = this.db.query("SELECT id, chord FROM keybindings").all();
return Object.fromEntries(rows.map((r) => [r.id, r.chord]));
}
setKeybinding(id, chord) {
this.db.query("INSERT INTO keybindings (id, chord) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET chord = excluded.chord").run(id, chord);
}
deleteKeybinding(id) {
this.db.query("DELETE FROM keybindings WHERE id = ?").run(id);
}
close() {
this.db.close();
}
@ -231964,6 +231996,7 @@ class SettingsDb {
var settingsDb = new SettingsDb;
// src/bun/index.ts
import { randomUUID } from "crypto";
var DEV_SERVER_PORT = 9760;
var DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
var ptyClient = new PtyClient;
@ -231987,8 +232020,18 @@ async function connectToDaemon(retries = 5, delayMs = 500) {
}
return false;
}
var BRANCH_RE = /^[a-zA-Z0-9/_.-]+$/;
async function gitWorktreeAdd(mainRepoPath, worktreePath, branchName) {
const proc = Bun.spawn(["git", "worktree", "add", worktreePath, "-b", branchName], { cwd: mainRepoPath, stderr: "pipe", stdout: "pipe" });
const exitCode = await proc.exited;
if (exitCode !== 0) {
const errText = await new Response(proc.stderr).text();
return { ok: false, error: errText.trim() || `git exited with code ${exitCode}` };
}
return { ok: true };
}
var rpc = BrowserView.defineRPC({
maxRequestTime: 1e4,
maxRequestTime: 15000,
handlers: {
requests: {
"pty.create": async ({ sessionId, cols, rows, cwd }) => {
@ -232107,6 +232150,127 @@ var rpc = BrowserView.defineRPC({
console.error("[themes.deleteCustom]", err);
return { ok: false };
}
},
"groups.list": () => {
try {
return { groups: settingsDb.listGroups() };
} catch (err) {
console.error("[groups.list]", err);
return { groups: [] };
}
},
"project.clone": async ({ projectId, branchName }) => {
try {
if (!BRANCH_RE.test(branchName)) {
return { ok: false, error: "Invalid branch name. Use only letters, numbers, /, _, -, ." };
}
const source = settingsDb.getProject(projectId);
if (!source) {
return { ok: false, error: `Project not found: ${projectId}` };
}
const mainRepoPath = source.mainRepoPath ?? source.cwd;
const allProjects = settingsDb.listProjects();
const existingClones = allProjects.filter((p) => p.cloneOf === projectId || source.cloneOf && p.cloneOf === source.cloneOf);
if (existingClones.length >= 3) {
return { ok: false, error: "Maximum 3 clones per project reached" };
}
const cloneIndex = existingClones.length + 1;
const worktreePath = `${mainRepoPath}-wt-${cloneIndex}`;
const gitResult = await gitWorktreeAdd(mainRepoPath, worktreePath, branchName);
if (!gitResult.ok) {
return { ok: false, error: gitResult.error };
}
const cloneId = `${projectId}-clone-${cloneIndex}-${randomUUID().slice(0, 8)}`;
const cloneConfig = {
id: cloneId,
name: `${source.name} [${branchName}]`,
cwd: worktreePath,
accent: source.accent,
provider: source.provider,
profile: source.profile,
model: source.model,
groupId: source.groupId ?? "dev",
mainRepoPath,
cloneOf: projectId,
worktreePath,
worktreeBranch: branchName,
cloneIndex
};
settingsDb.setProject(cloneId, cloneConfig);
return {
ok: true,
project: { id: cloneId, config: JSON.stringify(cloneConfig) }
};
} catch (err) {
const error2 = err instanceof Error ? err.message : String(err);
console.error("[project.clone]", err);
return { ok: false, error: error2 };
}
},
"window.minimize": () => {
try {
mainWindow.minimize();
return { ok: true };
} catch (err) {
console.error("[window.minimize]", err);
return { ok: false };
}
},
"window.maximize": () => {
try {
const frame2 = mainWindow.getFrame();
if (frame2.x <= 0 && frame2.y <= 0) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
return { ok: true };
} catch (err) {
console.error("[window.maximize]", err);
return { ok: false };
}
},
"window.close": () => {
try {
mainWindow.close();
return { ok: true };
} catch (err) {
console.error("[window.close]", err);
return { ok: false };
}
},
"window.getFrame": () => {
try {
return mainWindow.getFrame();
} catch {
return { x: 0, y: 0, width: 1400, height: 900 };
}
},
"keybindings.getAll": () => {
try {
return { keybindings: settingsDb.getKeybindings() };
} catch (err) {
console.error("[keybindings.getAll]", err);
return { keybindings: {} };
}
},
"keybindings.set": ({ id, chord }) => {
try {
settingsDb.setKeybinding(id, chord);
return { ok: true };
} catch (err) {
console.error("[keybindings.set]", err);
return { ok: false };
}
},
"keybindings.reset": ({ id }) => {
try {
settingsDb.deleteKeybinding(id);
return { ok: true };
} catch (err) {
console.error("[keybindings.reset]", err);
return { ok: false };
}
}
},
messages: {}
@ -232145,15 +232309,20 @@ async function getMainViewUrl() {
}
connectToDaemon();
var url = await getMainViewUrl();
var savedX = Number(settingsDb.getSetting("win_x") ?? 100);
var savedY = Number(settingsDb.getSetting("win_y") ?? 100);
var savedWidth = Number(settingsDb.getSetting("win_width") ?? 1400);
var savedHeight = Number(settingsDb.getSetting("win_height") ?? 900);
var mainWindow = new BrowserWindow({
title: "Agent Orchestrator \u2014 Electrobun",
title: "Agent Orchestrator",
titleBarStyle: "hidden",
url,
rpc,
frame: {
width: 1400,
height: 900,
x: 100,
y: 100
width: isNaN(savedWidth) ? 1400 : savedWidth,
height: isNaN(savedHeight) ? 900 : savedHeight,
x: isNaN(savedX) ? 100 : savedX,
y: isNaN(savedY) ? 100 : savedY
}
});
console.log("Agent Orchestrator (Electrobun) started!");

View file

@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Svelte App</title>
<script type="module" crossorigin src="/assets/index-CEtguZVp.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dvoh622C.css">
<script type="module" crossorigin src="/assets/index-CgAt0V08.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-rAZ5LabM.css">
</head>
<body>
<div id="app"></div>