Skip to content
Open
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
4 changes: 3 additions & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -3434,7 +3434,9 @@
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {}
"dependencyPaths": {
"../application/contracts/settings-presentation/runtime-host-settings-target.js": 1
}
},
"src/renderer/settings/settings-status-badge.ts": {
"bridgePaths": {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,131 @@ test('stable page bridge retries rather than exposing a stale default-Host read'
assert.deepEqual(reads, ['host-a', 'host-b']);
});

test('page bridge keeps the last successful today snapshot through a failed refresh', async () => {
let fail = false;
let reads = 0;
const services = createFakeModuleHubServices({
dailyReview: dailyReviewService(async () => {
reads += 1;
if (fail) throw new Error('offline');
return { ok: true, data: summary(reads) };
}),
});
const bridge = createDailyReviewBridge(services, 'en');
assert.equal(bridge.readCachedDay?.(0, 1), undefined);
const first = await bridge.fetchDay(0, 1);
assert.equal(bridge.readCachedDay?.(0, 1), first);
assert.equal(bridge.readCachedDay?.(-1, 1), undefined);
assert.equal(bridge.readCachedDay?.(0, 7), undefined);
await bridge.fetchDay(-1, 1);
assert.equal(bridge.readCachedDay?.(0, 1), first);
fail = true;
await assert.rejects(bridge.fetchDay(0, 1), /offline/);
assert.equal(bridge.readCachedDay?.(0, 1), first);
fail = false;
const fresh = await bridge.fetchDay(0, 1);
assert.equal(bridge.readCachedDay?.(0, 1), fresh);
assert.equal(reads, 4);
});

test('cancelled and superseded reads cannot overwrite a newer cached summary', async () => {
const reads = Array.from({ length: 3 }, () => deferred<{ ok: true; data: DailyReviewSummary }>());
let index = 0;
const bridge = createDailyReviewBridge(createFakeModuleHubServices({
dailyReview: dailyReviewService(async () => reads[index++]!.promise),
}), 'en');
const oldRead = bridge.fetchDay(0, 1);
const newRead = bridge.fetchDay(0, 1);
reads[1]!.resolve({ ok: true, data: summary(8) });
const fresh = await newRead;
reads[0]!.resolve({ ok: true, data: summary(1) });
await oldRead;
assert.equal(bridge.readCachedDay?.(0, 1), fresh);

const request = new AbortController();
const cancelled = bridge.fetchDay(0, 1, request.signal);
await Promise.resolve();
request.abort();
reads[2]!.resolve({ ok: true, data: summary(99) });
await assert.rejects(cancelled, { name: 'AbortError' });
assert.equal(bridge.readCachedDay?.(0, 1), fresh);
});

test('a new local day cannot reuse yesterday as today or cache an overnight read', async (t) => {
let now = new Date(2026, 8, 18, 23, 59).getTime();
t.mock.method(Date, 'now', () => now);
const pending = deferred<{ ok: true; data: DailyReviewSummary }>();
let reads = 0;
const bridge = createDailyReviewBridge(createFakeModuleHubServices({
dailyReview: dailyReviewService(async () => {
reads += 1;
return reads === 1 ? { ok: true, data: summary() } : pending.promise;
}),
}), 'en');
await bridge.fetchDay(0, 1);
assert.ok(bridge.readCachedDay?.(0, 1));
const overnight = bridge.fetchDay(0, 1);
now = new Date(2026, 8, 19, 0, 1).getTime();
assert.equal(bridge.readCachedDay?.(0, 1), undefined);
pending.resolve({ ok: true, data: summary(99) });
await overnight;
assert.equal(bridge.readCachedDay?.(0, 1), undefined);
});

test('the controller invalidates cached and pending reads on Host changes and disposes its subscription', async () => {
const { root } = installReactRenderer();
const hostA = { profileId: 'a', hostId: 'a' };
const hostB = { profileId: 'b', hostId: 'b' };
let currentHost = hostA;
let listener: Parameters<ModuleHubServices['runtimeHosts']['subscribeChanges']>[0] | undefined;
const pending = deferred<{ ok: true; data: DailyReviewSummary }>();
let reads = 0;
const services = createFakeModuleHubServices({
runtimeHosts: {
getDefault: async () => currentHost,
subscribeChanges: (handler) => {
listener = handler;
return () => { listener = undefined; };
},
},
dailyReview: dailyReviewService(async () => {
reads += 1;
return reads === 2 ? pending.promise : { ok: true, data: summary(reads) };
}),
});
let controller: DailyReviewController | undefined;
function Probe() {
controller = useDailyReviewController({
services, uiLocale: 'en',
toastApi: { success: () => undefined, error: () => undefined },
appendComposerText: () => undefined,
captureActiveComposerClaim: () => undefined,
isDailyReviewSurfaceActive: () => false,
});
return null;
}
await act(async () => root.render(createElement(Probe)));
const bridge = controller!.bridge;
await bridge.fetchDay(0, 1);
await act(async () => root.render(createElement(Probe)));
assert.equal(controller!.bridge, bridge);
assert.ok(bridge.readCachedDay?.(0, 1));
const loading = bridge.fetchDay(0, 1);
await Promise.resolve();
currentHost = hostB;
assert.ok(listener);
listener({ ...hostB, readiness: 'ready', isDefault: true });
assert.equal(bridge.readCachedDay?.(0, 1), undefined);
pending.resolve({ ok: true, data: summary(99) });
await loading;
assert.equal(bridge.readCachedDay?.(0, 1), undefined);
const fresh = await bridge.fetchDay(0, 1);
assert.equal(bridge.readCachedDay?.(0, 1), fresh);
await act(async () => root.unmount());
assert.equal(listener, undefined);
assert.equal(bridge.readCachedDay?.(0, 1), undefined);
});

test('today paste captures its composer claim before reading and drops a late result', async () => {
const { root } = installReactRenderer();
const pendingDay = deferred<{ ok: true; data: DailyReviewSummary }>();
Expand Down
33 changes: 32 additions & 1 deletion apps/desktop/src/main/__tests__/settings-resource-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import {
createDefaultSettings,
type RuntimeHostAppSettings,
} from '@maka/core/settings';
import type {
CapabilitySnapshotCollection,
PermissionSnapshot,
} from '@maka/core/capabilities';
import type { HealthSnapshot } from '@maka/core/health';
import {
beginSettingsResourceLoad,
completeSettingsResourceLoad,
Expand Down Expand Up @@ -202,7 +207,7 @@ describe('Settings snapshot cache', () => {
assert.equal(settingsSnapshotCacheFor(secondBridge).readClient(), undefined);
});

it('isolates settings and connections by selected Runtime Host key', () => {
it('isolates every Runtime Host snapshot by selected Host key', () => {
const cache = createSettingsSnapshotCache();
const localSettings = runtimeHostSettings();
const remoteSettings = {
Expand All @@ -213,15 +218,30 @@ describe('Settings snapshot cache', () => {
},
};
const localConnections = { connections: [], defaultSlug: 'local-default' };
const localHealth: HealthSnapshot = {
checkedAt: 1,
signals: [],
summary: { ok: 0, info: 0, warning: 0, error: 0, unknown: 0 },
};
const localPermissionCenter = {
permissions: { checkedAt: 1, platform: 'darwin', permissions: {} } as PermissionSnapshot,
capabilities: { checkedAt: 1, capabilities: [] } as CapabilitySnapshotCollection,
};

cache.commitRuntimeHostSettingsRead(LOCAL_KEY, localSettings);
cache.commitRuntimeHostSettingsRead(REMOTE_KEY, remoteSettings);
cache.commitRuntimeHostConnectionsRead(LOCAL_KEY, localConnections);
cache.commitRuntimeHostHealthRead(LOCAL_KEY, localHealth);
cache.commitRuntimeHostPermissionCenterRead(LOCAL_KEY, localPermissionCenter);

assert.equal(cache.readRuntimeHostSettings(LOCAL_KEY), localSettings);
assert.equal(cache.readRuntimeHostSettings(REMOTE_KEY), remoteSettings);
assert.equal(cache.readRuntimeHostConnections(LOCAL_KEY), localConnections);
assert.equal(cache.readRuntimeHostConnections(REMOTE_KEY), undefined);
assert.equal(cache.readRuntimeHostHealth(LOCAL_KEY), localHealth);
assert.equal(cache.readRuntimeHostHealth(REMOTE_KEY), undefined);
assert.equal(cache.readRuntimeHostPermissionCenter(LOCAL_KEY), localPermissionCenter);
assert.equal(cache.readRuntimeHostPermissionCenter(REMOTE_KEY), undefined);
});

it('prunes snapshots when a profile reconnects with a new host id', () => {
Expand All @@ -231,6 +251,15 @@ describe('Settings snapshot cache', () => {
connections: [],
defaultSlug: null,
});
cache.commitRuntimeHostHealthRead(LOCAL_KEY, {
checkedAt: 1,
signals: [],
summary: { ok: 0, info: 0, warning: 0, error: 0, unknown: 0 },
});
cache.commitRuntimeHostPermissionCenterRead(LOCAL_KEY, {
permissions: { checkedAt: 1, platform: 'darwin', permissions: {} } as PermissionSnapshot,
capabilities: { checkedAt: 1, capabilities: [] },
});

cache.commitRuntimeHostCatalogRead(catalog([
{
Expand All @@ -244,6 +273,8 @@ describe('Settings snapshot cache', () => {

assert.equal(cache.readRuntimeHostSettings(LOCAL_KEY), undefined);
assert.equal(cache.readRuntimeHostConnections(LOCAL_KEY), undefined);
assert.equal(cache.readRuntimeHostHealth(LOCAL_KEY), undefined);
assert.equal(cache.readRuntimeHostPermissionCenter(LOCAL_KEY), undefined);
});

it('stores settings and connection reads independently', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import {
import { useToast } from "@maka/ui";
export interface SettingsHostTarget { readonly profileId: string; readonly hostId: string; }

export function runtimeHostSettingsKey(host: SettingsHostTarget): string {
return `${host.profileId}:${host.hostId}`;
}

interface RuntimeHostSettingsTargetValue {
readonly host: SettingsHostTarget;
/**
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/renderer/features/module-hub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ reach it only through `testing.ts`.
- The Daily Review page bridge is stable for one services/locale pair. Page
feedback is live-surface fenced, while Command Palette commands remain usable
off-page.
- That bridge retains the last successful today snapshot across leaf mounts;
every mount still revalidates. Host lifecycle events and local-day rollover
invalidate it. Cancelled, superseded, and pre-invalidation reads cannot commit.
- Daily Review paste captures the active Composer before its first await and
validates the Session, navigation owner, and Composer handle before append and
feedback.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
* under the License.
*/

import { useMemo, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { localDayBoundsAt } from '@maka/core/daily-review';
import type {
DailyReviewArchive,
DailyReviewArchiveSummary,
Expand Down Expand Up @@ -67,7 +68,8 @@ export interface ActiveComposerClaim {

/** Structural equivalent of the UI bridge; kept here so @maka/ui stays leaf-only. */
export interface DailyReviewBridge {
fetchDay(offsetDays: number, daySpan?: number): Promise<DailyReviewSummary>;
readCachedDay?(offsetDays: number, daySpan?: number): DailyReviewSummary | undefined;
fetchDay(offsetDays: number, daySpan?: number, signal?: AbortSignal): Promise<DailyReviewSummary>;
runOnce?(input: {
range: DailyReviewRange;
offsetDays?: number;
Expand Down Expand Up @@ -147,11 +149,30 @@ async function readCurrentDefaultHost<T>(
export function createDailyReviewBridge(
services: ModuleHubServices,
locale: UiLocale,
): DailyReviewBridge {
): DailyReviewBridge & { invalidateCache(): void } {
const copy = getShellRemainingCopy(locale).dailyReview;
// Every leaf mount starts on today. Keep only that snapshot, rather than
// retaining an unbounded history of date/range selections in the renderer.
let cachedToday: { dayStart: number; summary: DailyReviewSummary } | undefined;
let generation = 0;
let latestTodayRead = 0;
return {
async fetchDay(offsetDays: number, daySpan?: number) {
return readCurrentDefaultHost(services, async (host) => {
invalidateCache() {
cachedToday = undefined;
generation += 1;
},
readCachedDay(offsetDays, daySpan = 1) {
if (offsetDays !== 0 || daySpan !== 1) return undefined;
if (cachedToday?.dayStart !== localDayBoundsAt(Date.now(), 0).fromMs) return undefined;
return cachedToday.summary;
},
async fetchDay(offsetDays: number, daySpan = 1, signal?: AbortSignal) {
const cachesToday = offsetDays === 0 && daySpan === 1;
const read = cachesToday ? ++latestTodayRead : 0;
const readGeneration = generation;
const dayStart = localDayBoundsAt(Date.now(), 0).fromMs;
const summary = await readCurrentDefaultHost(services, async (host) => {
signal?.throwIfAborted();
const result = await services.dailyReview.day(
offsetDays,
daySpan,
Expand All @@ -160,6 +181,12 @@ export function createDailyReviewBridge(
if (!result.ok) throw new Error(result.error.message);
return result.data;
});
signal?.throwIfAborted();
if (
cachesToday && read === latestTodayRead && readGeneration === generation
&& dayStart === localDayBoundsAt(Date.now(), 0).fromMs
) cachedToday = { dayStart, summary };
return summary;
},
runOnce(input) {
return services.dailyReview.runOnce(input);
Expand All @@ -186,6 +213,13 @@ export function useDailyReviewController(
() => createDailyReviewBridge(input.services, input.uiLocale),
[input.services, input.uiLocale],
);
useEffect(() => {
const unsubscribe = input.services.runtimeHosts.subscribeChanges(() => bridge.invalidateCache());
return () => {
unsubscribe();
bridge.invalidateCache();
};
}, [bridge, input.services.runtimeHosts]);

return useMemo(() => {
const copy = getShellCopy(input.uiLocale).commandActions;
Expand Down
Loading