Skip to content
Merged
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
14 changes: 14 additions & 0 deletions changelog/0.4.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# 0.4.1 — 2026-06-23

Cue Card placement & memory.

## Changed

- **The Cue Card opens at the bottom-right** of your screen by default, and is a
bit taller, so suggested answers have more room.

## Added

- **Cue Card remembers its size and position.** Move or resize it and it stays
that way next time you open the app. If the monitor it was on is gone, it falls
back to the bottom-right default. (Reset App Settings restores the default.)
1 change: 1 addition & 0 deletions changelog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ the project uses [Semantic Versioning](https://semver.org/).

| Version | Date | Highlights |
| --- | --- | --- |
| [0.4.1](./0.4.1.md) | 2026-06-23 | Cue Card opens bottom-right, taller, and remembers size/position |
| [0.4.0](./0.4.0.md) | 2026-06-23 | Rebrand to BrainCue Copilot, Cue Card (default-on), guarded Privacy Mode |
| [0.3.0](./0.3.0.md) | 2026-06-23 | Custom titlebar, sidebar status panel, reset/wipe, exit hotkey |
| [0.2.0](./0.2.0.md) | 2026-06-23 | System tray, configurable global shortcuts, animated logo |
Expand Down
14 changes: 14 additions & 0 deletions docs/sessions/2026-06-23.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,17 @@ leaves 0 Electron processes. Committed on `feature/v0.2.0-tray-shortcuts-changel
- Confirm the **x-margin** fix targeted the right place (I widened dashboard page
content; if you meant the Cue Card or titlebar, point me at it).
- Decide on the final Cue Card name (I went with "Cue Card"; easy to change).

## v0.4.1 — Cue Card placement & persistence

- Cue Card defaults to the **bottom-right** of the primary display's work area
(`EDGE_MARGIN` 24px) and is taller: compact `440×460`, expanded `520×680`.
- **Persist geometry**: new `SETTINGS_KEYS.overlayBounds`; `overlayWindow.ts`
saves `getBounds()` (debounced 400ms) on `move`/`resize`, and restores it on
creation via `initialBounds()` with an `isOnScreen()` guard (falls back to the
bottom-right default if the saved monitor is gone). Added to `APP_SETTING_KEYS`
so factory reset restores the default.
- `setOverlayMode` now anchors the bottom-right corner and clamps to the work
area, so expanding doesn't push the card off-screen.
- Smoke-tested: clean boot, 0 shortcut-registration failures (earlier failures
were a leftover dev instance holding the global shortcuts, not a regression).
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ai-interview-assistant",
"version": "0.4.0",
"version": "0.4.1",
"description": "BrainCue Copilot — desktop AI interview copilot (Electron + React + OpenAI). Local-first data, BYO OpenAI key.",
"author": "",
"license": "MIT",
Expand Down
2 changes: 2 additions & 0 deletions src/main/db/repositories/settings.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const SETTINGS_KEYS = {
dataConsentAck: 'data_consent_ack',
tourDone: 'tour_done',
shortcuts: 'shortcuts',
overlayBounds: 'overlay_bounds',
} as const;

/** Non-secret settings cleared by a factory reset (everything except the API key). */
Expand All @@ -61,4 +62,5 @@ const APP_SETTING_KEYS: string[] = [
SETTINGS_KEYS.dataConsentAck,
SETTINGS_KEYS.tourDone,
SETTINGS_KEYS.shortcuts,
SETTINGS_KEYS.overlayBounds,
];
92 changes: 87 additions & 5 deletions src/main/windows/overlayWindow.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,69 @@
import { BrowserWindow } from 'electron';
import { BrowserWindow, screen } from 'electron';
import { join } from 'path';
import { EVENTS } from '@shared/ipc';
import { SETTINGS_KEYS, settingsRepo } from '../db/repositories/settings.repo';
import { attachDiagnostics, loadRenderer } from './loadRenderer';
import { applyPrivacyToWindow } from '../services/session/privacy';
import { getMainWindow } from './mainWindow';
import type { OverlayMode } from '@shared/types';

let overlay: BrowserWindow | null = null;

interface Bounds {
x: number;
y: number;
width: number;
height: number;
}

// Margin from the screen edge for the default (bottom-right) placement.
const EDGE_MARGIN = 24;

/** Bottom-right of the primary display's work area, at the default size. */
function defaultBounds(): Bounds {
const wa = screen.getPrimaryDisplay().workArea;
const { width, height } = SIZES.compact;
return {
width,
height,
x: Math.round(wa.x + wa.width - width - EDGE_MARGIN),
y: Math.round(wa.y + wa.height - height - EDGE_MARGIN),
};
}

/** True if the window's center sits within some connected display — so a window
* saved on a now-disconnected monitor doesn't open off-screen. */
function isOnScreen(b: Bounds): boolean {
const cx = b.x + b.width / 2;
const cy = b.y + b.height / 2;
return screen.getAllDisplays().some((d) => {
const a = d.workArea;
return cx >= a.x && cx <= a.x + a.width && cy >= a.y && cy <= a.y + a.height;
});
}

/** Restore the last saved Cue Card geometry, falling back to the bottom-right
* default (first run, or a saved monitor that's no longer connected). */
function initialBounds(): Bounds {
const saved = settingsRepo.getJson<Bounds | null>(SETTINGS_KEYS.overlayBounds, null);
if (saved && [saved.x, saved.y, saved.width, saved.height].every(Number.isFinite) && isOnScreen(saved)) {
return saved;
}
return defaultBounds();
}

let saveTimer: ReturnType<typeof setTimeout> | null = null;
/** Persist the current geometry (debounced) so size + position survive restarts. */
function scheduleSaveBounds(): void {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
saveTimer = null;
if (overlay && !overlay.isDestroyed()) {
settingsRepo.setJson(SETTINGS_KEYS.overlayBounds, overlay.getBounds());
}
}, 400);
}

/** Tell the dashboard whether the overlay is currently visible, so its
* "Show/Hide overlay" button reflects toggles from the hotkey, tray, the
* overlay's own close button, or a session ending. */
Expand All @@ -16,8 +72,8 @@ function notifyVisibility(visible: boolean): void {
}

const SIZES: Record<OverlayMode, { width: number; height: number }> = {
compact: { width: 420, height: 200 },
expanded: { width: 480, height: 520 },
compact: { width: 440, height: 460 },
expanded: { width: 520, height: 680 },
};

/** Created once at startup (and idempotent thereafter); kept hidden when not in
Expand All @@ -26,7 +82,9 @@ export function createOverlayWindow(): BrowserWindow {
if (overlay && !overlay.isDestroyed()) return overlay;

overlay = new BrowserWindow({
...SIZES.compact,
...initialBounds(), // restored geometry, or bottom-right default on first run
minWidth: 360,
minHeight: 240,
show: false,
frame: false,
// Opaque (not transparent): on Windows, setContentProtection /
Expand Down Expand Up @@ -60,6 +118,10 @@ export function createOverlayWindow(): BrowserWindow {
});
overlay.on('hide', () => notifyVisibility(false));

// Persist the Cue Card's size + position (debounced) so they survive restarts.
overlay.on('move', scheduleSaveBounds);
overlay.on('resize', scheduleSaveBounds);

attachDiagnostics(overlay, 'overlay');
loadRenderer(overlay, 'overlay');

Expand Down Expand Up @@ -94,5 +156,25 @@ export function setOverlayMode(mode: OverlayMode): void {
const w = getOverlayWindow();
if (!w) return;
const size = SIZES[mode];
w.setBounds({ ...w.getBounds(), ...size });
const cur = w.getBounds();
// Anchor the bottom-right corner (the card's default home) so growing to the
// expanded size doesn't push it off the bottom/right edge, then clamp on-screen.
const next: Bounds = {
width: size.width,
height: size.height,
x: cur.x + cur.width - size.width,
y: cur.y + cur.height - size.height,
};
w.setBounds(clampToWorkArea(next));
}

/** Keep a window fully inside the work area of the display it's mostly on. */
function clampToWorkArea(b: Bounds): Bounds {
const wa = screen.getDisplayMatching(b).workArea;
return {
width: b.width,
height: b.height,
x: Math.round(Math.min(Math.max(b.x, wa.x), wa.x + wa.width - b.width)),
y: Math.round(Math.min(Math.max(b.y, wa.y), wa.y + wa.height - b.height)),
};
}