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

@ -39,6 +39,26 @@ 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
);
`;
// Seed default groups (idempotent via INSERT OR IGNORE)
const SEED_GROUPS = `
INSERT OR IGNORE INTO groups VALUES ('dev', 'Development', '🔧', 0);
INSERT OR IGNORE INTO groups VALUES ('test', 'Testing', '🧪', 1);
INSERT OR IGNORE INTO groups VALUES ('ops', 'DevOps', '🚀', 2);
INSERT OR IGNORE INTO groups VALUES ('research', 'Research', '🔬', 3);
`;
// ── SettingsDb class ─────────────────────────────────────────────────────────
@ -51,6 +71,18 @@ export interface ProjectConfig {
provider?: string;
profile?: string;
model?: string;
/** Group this project belongs to. Defaults to 'dev'. */
groupId?: string;
/** For clones: path of the source repo (worktree parent). */
mainRepoPath?: string;
/** For clones: ID of the original project this was cloned from. */
cloneOf?: string;
/** For clones: absolute path to the git worktree. */
worktreePath?: string;
/** For clones: branch name checked out in the worktree. */
worktreeBranch?: string;
/** 1-indexed clone number within the parent (13). */
cloneIndex?: number;
[key: string]: unknown;
}
@ -60,6 +92,13 @@ export interface CustomTheme {
palette: Record<string, string>;
}
export interface Group {
id: string;
name: string;
icon: string;
position: number;
}
export class SettingsDb {
private db: Database;
@ -69,6 +108,7 @@ export class SettingsDb {
this.db = new Database(DB_PATH);
this.db.exec(SCHEMA);
this.db.exec(SEED_GROUPS);
// Seed schema_version row if missing
const version = this.db
@ -136,6 +176,14 @@ export class SettingsDb {
});
}
// ── Groups ─────────────────────────────────────────────────────────────────
listGroups(): Group[] {
return this.db
.query<Group, []>("SELECT id, name, icon, position FROM groups ORDER BY position ASC")
.all();
}
// ── Custom Themes ─────────────────────────────────────────────────────────
getCustomThemes(): CustomTheme[] {
@ -166,6 +214,25 @@ export class SettingsDb {
this.db.query("DELETE FROM custom_themes WHERE id = ?").run(id);
}
// ── Keybindings ───────────────────────────────────────────────────────────
getKeybindings(): Record<string, string> {
const rows = this.db
.query<{ id: string; chord: string }, []>("SELECT id, chord FROM keybindings")
.all();
return Object.fromEntries(rows.map((r) => [r.id, r.chord]));
}
setKeybinding(id: string, chord: string): void {
this.db
.query("INSERT INTO keybindings (id, chord) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET chord = excluded.chord")
.run(id, chord);
}
deleteKeybinding(id: string): void {
this.db.query("DELETE FROM keybindings WHERE id = ?").run(id);
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
close(): void {