Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b008ae2
feat(log-viewer): sidebar filters, native window chrome, paginated list
rodrigok Aug 8, 2026
d763d7e
feat(log-viewer): distribution timeline with drag range selection
rodrigok Aug 8, 2026
6b10fbd
feat(log-viewer): inset log card, live transparency, restore on launch
rodrigok Aug 8, 2026
9a03c72
feat(downloads): move downloads into its own window on shared chrome
rodrigok Aug 9, 2026
d5ea630
feat(downloads): grey document icon for file types
rodrigok Aug 9, 2026
1ac6b86
feat(settings): settings in its own window, with remembered positions
rodrigok Aug 9, 2026
34670d9
feat(windows): in-app title bar on Windows, and room for the theme op…
rodrigok Aug 10, 2026
48e34a8
feat(settings): fold About into settings, and one spacing rhythm
rodrigok Aug 10, 2026
31f3412
Merge branch 'master' into feat/log-viewer-revamp
rodrigok Aug 10, 2026
cfecaa9
fix(test): drive the clock in the downloads indicator seen test
rodrigok Aug 10, 2026
aaf4664
feat(document-viewer): open PDFs and markdown in their own window
rodrigok Aug 10, 2026
37ef21a
feat(document-viewer): download the file, and read markdown as source
rodrigok Aug 10, 2026
072093c
Merge branch 'master' into feat/log-viewer-revamp
rodrigok Aug 10, 2026
d21e53e
refactor(chrome): one title bar height across every window
rodrigok Aug 10, 2026
a76cf62
Merge branch 'master' into feat/log-viewer-revamp
rodrigok Aug 10, 2026
c21695a
fix(chrome): stop the filter checkbox toggling twice
rodrigok Aug 10, 2026
02f8175
feat(chrome): let a filter be cleared completely
rodrigok Aug 10, 2026
0f84aa1
fix(windows): stop secondary windows opening full screen
rodrigok Aug 10, 2026
4f0c568
feat(downloads): show the same download row in the top bar panel
rodrigok Aug 11, 2026
c9e6710
fix(test): keep the downloads panel size test inside Jest's timeout
rodrigok Aug 11, 2026
09fa0a3
fix: address review findings on the secondary windows
rodrigok Aug 12, 2026
25ed2aa
fix(windows): restore minimized secondary windows on reopen
jeanfbrito Aug 12, 2026
1d174eb
fix(document-viewer): surface save errors and stop dropping opens
jeanfbrito Aug 12, 2026
25778d7
fix(chrome): keep out-of-universe values visible when facets narrow
jeanfbrito Aug 12, 2026
69da9ca
fix(settings): settle update checks properly and keep search ephemeral
jeanfbrito Aug 12, 2026
ac2cb22
fix(app): never drop the last persisted state write
jeanfbrito Aug 12, 2026
42082c8
fix(log-viewer): surface load failures and save feedback
jeanfbrito Aug 12, 2026
a50aae3
fix(log-viewer): give the timeline real slider keyboard semantics
jeanfbrito Aug 12, 2026
9113ef3
fix(chrome): tokenize secondary-window card shadow and radius
jeanfbrito Aug 12, 2026
7a0d4c0
fix(settings): give sidebar tabs real keyboard and selected weight
jeanfbrito Aug 12, 2026
9a012ed
fix(chrome): tokenize hover fills and announce copy and save
jeanfbrito Aug 12, 2026
83fdc45
fix(log-viewer): use system tokens and distinguish display toggles
jeanfbrito Aug 12, 2026
1103968
fix(windows): confirm destructive clears and tokenize progress
jeanfbrito Aug 12, 2026
983ed6b
fix(app): persist immediately when the throttle interval elapses
jeanfbrito Aug 12, 2026
520fe19
fix(document-viewer): surface rejected save invocations
jeanfbrito Aug 12, 2026
3821eac
fix(log-viewer): drop stale loads and hide logs on error
jeanfbrito Aug 12, 2026
7703f26
fix(chrome): share section labels and Cmd+F search focus
jeanfbrito Aug 12, 2026
6842803
fix(chrome): drop extra icon colors and literal list chrome
jeanfbrito Aug 12, 2026
66c9314
fix(ui): close leftover CodeRabbit polish notes
jeanfbrito Aug 12, 2026
43d46cd
fix(downloads): use font-info for the row progress fill
jeanfbrito Aug 12, 2026
4666c44
fix(settings): answer the secondary-window IPC invoke in window specs
jeanfbrito Aug 12, 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
90 changes: 90 additions & 0 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,96 @@ export default [
},
],
},
{
external: makeExternal(['@bugsnag/js']),
input: 'src/downloadsWindow/downloads-window.tsx',
preserveEntrySignatures: 'strict',
plugins: [
json(),
replace({
'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
'preventAssignment': true,
}),
babel({
babelHelpers: 'bundled',
extensions,
}),
nodeResolve({
browser: true,
extensions,
}),
commonjs(),
run(),
],
output: [
{
dir: 'app',
format: 'cjs',
sourcemap: 'inline',
interop: 'auto',
},
],
},
{
external: makeExternal(['@bugsnag/js']),
input: 'src/settingsWindow/settings-window.tsx',
preserveEntrySignatures: 'strict',
plugins: [
json(),
replace({
'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
'preventAssignment': true,
}),
babel({
babelHelpers: 'bundled',
extensions,
}),
nodeResolve({
browser: true,
extensions,
}),
commonjs(),
run(),
],
output: [
{
dir: 'app',
format: 'cjs',
sourcemap: 'inline',
interop: 'auto',
},
],
},
{
external: makeExternal(['@bugsnag/js']),
input: 'src/documentViewerWindow/document-viewer-window.tsx',
preserveEntrySignatures: 'strict',
plugins: [
json(),
replace({
'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
'preventAssignment': true,
}),
babel({
babelHelpers: 'bundled',
extensions,
}),
nodeResolve({
browser: true,
extensions,
}),
commonjs(),
run(),
],
output: [
{
dir: 'app',
format: 'cjs',
sourcemap: 'inline',
interop: 'auto',
},
],
},
{
external: makeExternal(['@bugsnag/js']),
input: 'src/screenSharing/screen-picker-window.tsx',
Expand Down
18 changes: 18 additions & 0 deletions src/app/PersistableValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ type PersistableValues_4_15_0 = PersistableValues_4_14_0 & {

type PersistableValues_4_16_0 = PersistableValues_4_15_0 & {
navigationLayout: 'tabs' | 'sidebar' | 'hidden';
isLogViewerWindowOpen: boolean;
isDownloadsWindowOpen: boolean;
isSettingsWindowOpen: boolean;
secondaryWindowStates: Record<
string,
{ x: number; y: number; width: number; height: number }
>;
};

type PersistableValues_4_16_1 = PersistableValues_4_16_0 & {
Expand Down Expand Up @@ -257,6 +264,17 @@ export const migrations = {
...before,
navigationLayout:
(before as Partial<PersistableValues_4_16_0>).navigationLayout ?? 'tabs',
isLogViewerWindowOpen:
(before as Partial<PersistableValues_4_16_0>).isLogViewerWindowOpen ??
false,
isDownloadsWindowOpen:
(before as Partial<PersistableValues_4_16_0>).isDownloadsWindowOpen ??
false,
isSettingsWindowOpen:
(before as Partial<PersistableValues_4_16_0>).isSettingsWindowOpen ??
false,
secondaryWindowStates:
(before as Partial<PersistableValues_4_16_0>).secondaryWindowStates ?? {},
}),
'>=4.16.1': (before: PersistableValues_4_16_0): PersistableValues_4_16_1 => ({
...before,
Expand Down
11 changes: 11 additions & 0 deletions src/app/main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,17 @@ export const setupGpuCrashHandler = (): void => {
};

export const setupApp = (): void => {
// The About menu item shows the system panel on macOS; without this it would
// report Electron's own name and version in an unpackaged run, since those
// otherwise come from the bundle's Info.plist.
if (process.platform === 'darwin') {
app.setAboutPanelOptions({
applicationName: packageJson.productName,
applicationVersion: app.getVersion(),
copyright: packageJson.copyright,
});
}

app.addListener('activate', async () => {
try {
const browserWindow = await getRootWindow();
Expand Down
122 changes: 122 additions & 0 deletions src/app/main/persistence.main.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { PersistableValues } from '../PersistableValues';
import type * as PersistenceModule from './persistence';

const mockSet = jest.fn();

jest.mock('electron', () => ({
app: {
getVersion: jest.fn().mockReturnValue('1.0.0'),
},
}));

jest.mock('electron-store', () => {
return jest.fn().mockImplementation(() => ({
store: {},
set: mockSet,
get: jest.fn(),
}));
});

const value = (n: number): PersistableValues =>
({ marker: n }) as unknown as PersistableValues;

describe('persistValues throttling', () => {
let persistValues: typeof PersistenceModule.persistValues;
let flushPersistedValues: typeof PersistenceModule.flushPersistedValues;

beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
mockSet.mockClear();
jest.resetModules();
// eslint-disable-next-line @typescript-eslint/no-var-requires
const persistence: typeof PersistenceModule = require('./persistence');
persistValues = persistence.persistValues;
flushPersistedValues = persistence.flushPersistedValues;
});

afterEach(() => {
flushPersistedValues();
jest.useRealTimers();
});

it('writes immediately when outside the throttle window', () => {
jest.advanceTimersByTime(2000);
persistValues(value(1));

expect(mockSet).toHaveBeenCalledTimes(1);
expect(mockSet).toHaveBeenCalledWith(value(1));
});

it('writes immediately when the throttle interval has elapsed exactly', () => {
jest.advanceTimersByTime(2000);
persistValues(value(1));
mockSet.mockClear();

jest.advanceTimersByTime(1000);
persistValues(value(2));

expect(mockSet).toHaveBeenCalledTimes(1);
expect(mockSet).toHaveBeenCalledWith(value(2));
});

it('coalesces rapid successive calls and persists only the final values after the trailing interval', () => {
jest.advanceTimersByTime(2000);
persistValues(value(1));
mockSet.mockClear();

jest.advanceTimersByTime(100);
persistValues(value(2));
jest.advanceTimersByTime(200);
persistValues(value(3));
jest.advanceTimersByTime(200);
persistValues(value(4));

expect(mockSet).not.toHaveBeenCalled();

jest.advanceTimersByTime(500);

expect(mockSet).toHaveBeenCalledTimes(1);
expect(mockSet).toHaveBeenCalledWith(value(4));
});

it('does not drop the final write when it lands inside the throttle window', () => {
jest.advanceTimersByTime(2000);
persistValues(value(1));
mockSet.mockClear();

jest.advanceTimersByTime(200);
persistValues(value(2));

jest.advanceTimersByTime(800);

expect(mockSet).toHaveBeenCalledTimes(1);
expect(mockSet).toHaveBeenCalledWith(value(2));
});

it('flush writes pending values immediately without waiting for the trailing timer', () => {
jest.advanceTimersByTime(2000);
persistValues(value(1));
mockSet.mockClear();

jest.advanceTimersByTime(200);
persistValues(value(2));

expect(mockSet).not.toHaveBeenCalled();

flushPersistedValues();

expect(mockSet).toHaveBeenCalledTimes(1);
expect(mockSet).toHaveBeenCalledWith(value(2));
});

it('flush is a no-op when there is no pending write', () => {
jest.advanceTimersByTime(2000);
persistValues(value(1));
mockSet.mockClear();

flushPersistedValues();

expect(mockSet).not.toHaveBeenCalled();
});
});
58 changes: 51 additions & 7 deletions src/app/main/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,59 @@ export const setPersistedMeta = (key: string, value: unknown): void => {
}
};

const THROTTLE_INTERVAL_MS = 1000;

let lastSavedTime = 0;
let pendingValues: PersistableValues | null = null;
let trailingTimeout: ReturnType<typeof setTimeout> | null = null;

export const persistValues = (values: PersistableValues): void => {
if (Date.now() - lastSavedTime > 1000) {
try {
getElectronStore().set(values);
} catch (error) {
error instanceof Error && console.error(error);
const writeNow = (values: PersistableValues): void => {
try {
getElectronStore().set(values);
} catch (error) {
error instanceof Error && console.error(error);
}
lastSavedTime = Date.now();
};

const scheduleTrailingSave = (delay: number): void => {
if (trailingTimeout) {
return;
}

trailingTimeout = setTimeout(() => {
trailingTimeout = null;
if (pendingValues) {
const values = pendingValues;
pendingValues = null;
writeNow(values);
}
lastSavedTime = Date.now();
}, delay);
};

export const persistValues = (values: PersistableValues): void => {
const elapsed = Date.now() - lastSavedTime;

if (elapsed >= THROTTLE_INTERVAL_MS) {
pendingValues = null;
writeNow(values);
return;
}

pendingValues = values;
scheduleTrailingSave(THROTTLE_INTERVAL_MS - elapsed);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

/** Synchronously flush any pending trailing write, e.g. before app quit. */
export const flushPersistedValues = (): void => {
if (trailingTimeout) {
clearTimeout(trailingTimeout);
trailingTimeout = null;
}

if (pendingValues) {
const values = pendingValues;
pendingValues = null;
writeNow(values);
}
};
8 changes: 8 additions & 0 deletions src/app/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ const selectPersistableValuesB = createStructuredSelector({
telephonyGlobalShortcutConfig,
}: RootState) => telephonyGlobalShortcutConfig,
isTelephonyEnabled: ({ isTelephonyEnabled }: RootState) => isTelephonyEnabled,
isLogViewerWindowOpen: ({ isLogViewerWindowOpen }: RootState) =>
isLogViewerWindowOpen,
isDownloadsWindowOpen: ({ isDownloadsWindowOpen }: RootState) =>
isDownloadsWindowOpen,
isSettingsWindowOpen: ({ isSettingsWindowOpen }: RootState) =>
isSettingsWindowOpen,
secondaryWindowStates: ({ secondaryWindowStates }: RootState) =>
secondaryWindowStates,
});

export const selectPersistableValues = createSelector(
Expand Down
Loading
Loading