feat(app): wire NotificationService, IntentService, ContinuityService into main
Task 30 of the slice plan. Replaces the Task 2 placeholder main
entry with the final whenReady wiring:
- profile path resolution + better-sqlite3 open + migrations.
- repo / store / continuity / intent / health constructed
against the open db.
- LocalOllamaProvider reads INKLING_OLLAMA_ENDPOINT for LAN
dogfood, falls back to localhost:11434 otherwise.
- AiWorker registers an onUpdate that fans note:updated through
pushNoteUpdated(getInboxWindow, note).
- NotificationService is plumbed to electron.Notification with
isSupported gating; CaptureService gets the worker.enqueue +
notify.celebrate hooks.
- IPC bindings for both capture and inbox surfaces.
- HotkeyService registers Ctrl/Cmd+Shift+J -> showQuickCapture;
failure logged but not fatal.
- Tray menu '구출한 메모 보기' / '기억 구출하기' / '종료'
with click-to-show-inbox.
- worker.loadFromDb() resumes pending jobs at startup.
- MediaGc runs once and logs the count of removed orphan dirs.
- Two logger.info(..., {...obj} as Record<string, unknown>)
casts at the health and gc result sites to satisfy the
index-signature requirement on logger meta — the result
objects (HealthResult, {removed: number}) are assignment-
compatible with Record<string, unknown> at runtime but TS
refuses without the spread + cast.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,86 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { app, BrowserWindow, Notification } from 'electron';
|
||||
import '@shared/types';
|
||||
import { createInboxWindow } from './windows/inboxWindow.js';
|
||||
import { initLogger, logger } from './logger.js';
|
||||
import { resolveProfilePaths } from './paths.js';
|
||||
import { openDb } from './db/index.js';
|
||||
import { NoteRepository } from './repository/NoteRepository.js';
|
||||
import { MediaStore } from './services/MediaStore.js';
|
||||
import { ContinuityService } from './services/ContinuityService.js';
|
||||
import { CaptureService } from './services/CaptureService.js';
|
||||
import { NotificationService } from './services/NotificationService.js';
|
||||
import { HotkeyService } from './services/HotkeyService.js';
|
||||
import { IntentService } from './services/IntentService.js';
|
||||
import { HealthChecker } from './services/HealthChecker.js';
|
||||
import { LocalOllamaProvider } from './ai/LocalOllamaProvider.js';
|
||||
import { AiWorker } from './ai/AiWorker.js';
|
||||
import { registerCaptureApi } from './ipc/captureApi.js';
|
||||
import { registerInboxApi, pushNoteUpdated } from './ipc/inboxApi.js';
|
||||
import { createInboxWindow, getInboxWindow } from './windows/inboxWindow.js';
|
||||
import {
|
||||
createQuickCaptureWindow, showQuickCapture, getQuickCaptureWindow
|
||||
} from './windows/quickCaptureWindow.js';
|
||||
import { createTray } from './tray.js';
|
||||
import { MediaGc } from './services/MediaGc.js';
|
||||
|
||||
app.whenReady().then(() => {
|
||||
app.whenReady().then(async () => {
|
||||
initLogger();
|
||||
logger.info('app.start', { platform: process.platform, version: app.getVersion() });
|
||||
|
||||
const paths = resolveProfilePaths('default');
|
||||
const db = openDb(paths.dbFile);
|
||||
const repo = new NoteRepository(db);
|
||||
const store = new MediaStore(paths.profileDir);
|
||||
const continuity = new ContinuityService(db);
|
||||
const intent = new IntentService(repo);
|
||||
|
||||
const provider = new LocalOllamaProvider({
|
||||
endpoint: process.env.INKLING_OLLAMA_ENDPOINT
|
||||
});
|
||||
const health = new HealthChecker(provider);
|
||||
void health.runOnce().then((h) => logger.info('ai.health', { ...h } as Record<string, unknown>));
|
||||
|
||||
const worker = new AiWorker(repo, provider, {
|
||||
onUpdate: (note) => pushNoteUpdated(getInboxWindow, note),
|
||||
logger
|
||||
});
|
||||
|
||||
const notify = new NotificationService({
|
||||
isSupported: () => Notification.isSupported(),
|
||||
send: (body) => {
|
||||
new Notification({ title: 'Inkling', body, silent: false }).show();
|
||||
}
|
||||
});
|
||||
|
||||
const capture = new CaptureService(repo, store, {
|
||||
enqueue: (id) => worker.enqueue(id),
|
||||
celebrate: (id) => notify.celebrate(id)
|
||||
});
|
||||
|
||||
registerCaptureApi(capture, getQuickCaptureWindow);
|
||||
registerInboxApi({
|
||||
repo, continuity, capture, health, intent,
|
||||
getInboxWindow
|
||||
});
|
||||
|
||||
const hotkeys = new HotkeyService();
|
||||
const reg = hotkeys.register({
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+Shift+J' : 'Ctrl+Shift+J',
|
||||
onTrigger: () => showQuickCapture()
|
||||
});
|
||||
if (!reg.ok) logger.warn('hotkey.register.failed', { reason: reg.reason });
|
||||
|
||||
createInboxWindow();
|
||||
createQuickCaptureWindow();
|
||||
createTray(
|
||||
() => createInboxWindow(),
|
||||
() => showQuickCapture()
|
||||
);
|
||||
|
||||
await worker.loadFromDb();
|
||||
|
||||
const gc = new MediaGc(db, store);
|
||||
void gc.run().then((r) => logger.info('media.gc', { ...r } as Record<string, unknown>));
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createInboxWindow();
|
||||
});
|
||||
|
||||
18
src/main/tray.ts
Normal file
18
src/main/tray.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { app, Tray, Menu, nativeImage } from 'electron';
|
||||
|
||||
let tray: Tray | null = null;
|
||||
|
||||
export function createTray(showInbox: () => void, showCapture: () => void): Tray {
|
||||
const icon = nativeImage.createEmpty();
|
||||
tray = new Tray(icon);
|
||||
tray.setToolTip('Inkling');
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{ label: '구출한 메모 보기', click: showInbox },
|
||||
{ label: '기억 구출하기', click: showCapture },
|
||||
{ type: 'separator' },
|
||||
{ label: '종료', click: () => { app.isQuitting = true; app.quit(); } }
|
||||
]);
|
||||
tray.setContextMenu(menu);
|
||||
tray.on('click', showInbox);
|
||||
return tray;
|
||||
}
|
||||
Reference in New Issue
Block a user