feat(ollama): InboxApi + preload + store recheckOllama + onOllamaStatus subscriber (#1 v0.2.3)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
altair823
2026-05-02 01:41:04 +09:00
parent 410a6f494b
commit c78f3af3a6
5 changed files with 73 additions and 2 deletions

View File

@@ -27,10 +27,16 @@ const api: InklingApi = {
getTrashCount: () => ipcRenderer.invoke('inbox:trashCount'),
listExpired: () => ipcRenderer.invoke('inbox:listExpired'),
trashExpiredBatch: (ids) => ipcRenderer.invoke('inbox:trashExpiredBatch', { ids }),
ollamaRecheck: () => ipcRenderer.invoke('inbox:ollamaRecheck'),
onNoteUpdated: (cb) => {
const listener = (_e: unknown, note: Note) => cb(note);
ipcRenderer.on('note:updated', listener);
return () => ipcRenderer.off('note:updated', listener);
},
onOllamaStatus: (cb) => {
const listener = (_e: unknown, status: { ok: boolean; reason?: string }) => cb(status);
ipcRenderer.on('ollama:status', listener);
return () => ipcRenderer.off('ollama:status', listener);
}
}
};

View File

@@ -22,13 +22,16 @@ export function App(): React.ReactElement {
useEffect(() => {
void loadInitial();
const unsub = inboxApi.onNoteUpdated((note) => {
const unsubNote = inboxApi.onNoteUpdated((note) => {
upsertNote(note);
void refreshMeta();
});
const unsubOllama = inboxApi.onOllamaStatus((status) => {
useInbox.setState({ ollamaStatus: status });
});
const onFocus = () => { void refreshMeta(); };
window.addEventListener('focus', onFocus);
return () => { unsub(); window.removeEventListener('focus', onFocus); };
return () => { unsubNote(); unsubOllama(); window.removeEventListener('focus', onFocus); };
}, [loadInitial, refreshMeta, upsertNote]);
const showRecovery = continuity.showRecoveryToast && !recoveryDismissed;

View File

@@ -30,6 +30,7 @@ interface InboxState {
loadExpired: () => Promise<void>;
trashExpiredBatch: (ids: string[]) => Promise<void>;
snoozeExpired: () => void;
recheckOllama: () => Promise<void>;
}
const emptyContinuity: WeeklyContinuity = {
@@ -167,5 +168,9 @@ export const useInbox = create<InboxState>((set, get) => ({
const kstMidnightFloor = Math.floor(kstNow / 86_400_000) * 86_400_000;
const nextKstMidnight = kstMidnightFloor + 86_400_000;
set({ expiredSnoozeUntilMs: nextKstMidnight - KST_OFFSET_MS });
},
async recheckOllama() {
const status = await inboxApi.ollamaRecheck();
set({ ollamaStatus: status });
}
}));

View File

@@ -79,7 +79,9 @@ export interface InboxApi {
getTrashCount(): Promise<number>;
listExpired(): Promise<Note[]>;
trashExpiredBatch(ids: string[]): Promise<{ trashedCount: number; confirmed: boolean }>;
ollamaRecheck(): Promise<{ ok: boolean; reason?: string }>;
onNoteUpdated(cb: (note: Note) => void): () => void;
onOllamaStatus(cb: (status: { ok: boolean; reason?: string }) => void): () => void;
}
export interface InklingApi {

View File

@@ -0,0 +1,55 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
const mockApi = {
listNotes: vi.fn(async () => []),
listTrash: vi.fn(async () => []),
getTrashCount: vi.fn(async () => 0),
getContinuity: vi.fn(async () => ({ weekStart: '', weekCount: 0, weekTarget: 7, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null })),
getPendingCount: vi.fn(async () => 0),
getOllamaStatus: vi.fn(async () => ({ ok: true })),
getTodayCount: vi.fn(async () => 0),
restoreNote: vi.fn(async () => {}),
permanentDeleteNote: vi.fn(async () => ({ confirmed: true })),
emptyTrash: vi.fn(async () => ({ confirmed: true, count: 0 })),
deleteNote: vi.fn(async () => {}),
onNoteUpdated: vi.fn(() => () => {}),
updateAiFields: vi.fn(async () => {}),
setDueDate: vi.fn(async () => {}),
setIntent: vi.fn(async () => {}),
dismissIntent: vi.fn(async () => {}),
listExpired: vi.fn(async () => []),
trashExpiredBatch: vi.fn(async () => ({ trashedCount: 0, confirmed: false })),
ollamaRecheck: vi.fn(async (): Promise<{ ok: boolean; reason?: string }> => ({ ok: true })),
onOllamaStatus: vi.fn(() => () => {})
};
vi.mock('../../src/renderer/inbox/api.js', () => ({ inboxApi: mockApi }));
describe('useInbox — ollama (v0.2.3 #1)', () => {
beforeEach(async () => {
const { useInbox } = await import('../../src/renderer/inbox/store.js');
useInbox.setState({
notes: [], trashNotes: [], trashCount: 0, showTrash: false,
loading: false, tagFilter: null, pendingCount: 0, todayCount: 0,
ollamaStatus: { ok: false, reason: 'refused' },
continuity: { weekStart: '', weekCount: 0, weekTarget: 7, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null },
expiredCandidates: [], expiredSnoozeUntilMs: null
});
Object.values(mockApi).forEach((fn) => 'mockClear' in fn && (fn as any).mockClear());
});
it('recheckOllama calls inboxApi.ollamaRecheck and updates ollamaStatus', async () => {
mockApi.ollamaRecheck.mockResolvedValueOnce({ ok: true });
const { useInbox } = await import('../../src/renderer/inbox/store.js');
await useInbox.getState().recheckOllama();
expect(mockApi.ollamaRecheck).toHaveBeenCalledTimes(1);
expect(useInbox.getState().ollamaStatus).toEqual({ ok: true });
});
it('recheckOllama propagates failure status', async () => {
mockApi.ollamaRecheck.mockResolvedValueOnce({ ok: false, reason: 'timeout' });
const { useInbox } = await import('../../src/renderer/inbox/store.js');
await useInbox.getState().recheckOllama();
expect(useInbox.getState().ollamaStatus).toEqual({ ok: false, reason: 'timeout' });
});
});