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:
parent
5032021915
commit
a020f59cb4
14 changed files with 1741 additions and 189 deletions
|
|
@ -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!");
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue