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
12 changes: 10 additions & 2 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ You do not need AWS, an Apple developer account, or an AI API key.
| macOS | 26.6 (arm64) | Verified. Linux and Windows are not verified. |
| Node.js | 24.21.0 | Verified. Development builds, tests, and the unpackaged app use it. |
| Yarn | 3.2.4 | The repository contains this version (`.yarnrc.yml` `yarnPath`). Any global `yarn` starts it. |
| Electron | 26.3.0 | Installed by Yarn. |
| Electron | 44.6.0 | Supported until 2027-03-02. The binary downloads on first use (see Setup). |

No native compiler is necessary. The install does not build `canvas`.

Expand All @@ -24,7 +24,7 @@ the built-in `node:sqlite` module.

Compatibility note: Angular 14 officially supports Node.js 14.15+ and 16.10+ only.
Node.js 24 builds and runs this branch, but it is outside the official range.
Electron 26 is end-of-life. Upgrade these in a separate step.
Upgrade Angular in a separate step (see [docs/follow-ups.md](docs/follow-ups.md)).

## Setup

Expand All @@ -36,6 +36,11 @@ yarn start

`yarn start-dev` does the build and the start in one command.

Since Electron 42, `yarn install` does not download the Electron binary.
The first `yarn start` (or the first end-to-end test) downloads it and
checks it against `node_modules/electron/checksums.json`. To download it
before you go offline: `node -e 'require("electron")'`.

## Architecture

| Part | Location | Runs in | Owns |
Expand Down Expand Up @@ -337,6 +342,9 @@ The end-to-end tests start the real app with new, empty profiles in
thumbnail and extract text from the managed copy, export the library,
restore it into a second profile through Settings > Backup, and show the
PDF there.
- `e2e/embedded-browser.e2e.mjs`: opens a website source in the desktop
Browser tab (`WebContentsView`), checks its URL, resizing, and navigation
state, and checks that closing removes it.
- `e2e/isolation.e2e.mjs`: two profiles at the same time get different chat
and storage addresses and tokens. Each server rejects the other
instance's requests. A second start of the same profile exits.
Expand Down
79 changes: 79 additions & 0 deletions docs/electron-upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Electron Upgrade (Milestone 4)

## Selected version

Electron **44.6.0** (Chromium 152, Node.js 24.18 inside Electron).

| Major | Stable since | End of support | Decision |
| --- | --- | --- | --- |
| 44 | 2026-08-25 | 2027-03-02 | Selected: newest stable, longest support |
| 43 | 2026-06-30 | 2027-01-05 | Shorter support |
| 42 | 2026-05-05 | 2026-10-20 | Support ends in two weeks |
| 45 | Planned 2026-10-20 | — | Not stable yet |

Sources: [Electron release schedule](https://releases.electronjs.org/schedule),
[Electron breaking changes](https://www.electronjs.org/docs/latest/breaking-changes).

## Breaking changes that affect this app (26 → 44)

| Version | Change | Action |
| --- | --- | --- |
| 32 | `File.path` removed | The preload script exposes `api.pathForFile(file)` with `webUtils.getPathForFile`. `NativeFiles.pathOf` uses it. |
| 30 | `BrowserView` deprecated | The embedded browser uses `WebContentsView`, `contentView.addChildView`, and `webContents.close()`. A window resize listener replaces `setAutoResize`. |
| 32 | `webContents.canGoBack()` and related methods deprecated | Use `webContents.navigationHistory`. |
| 21 | `printToPDF` options changed | Removed the old `marginType` option (default margins). |
| 42 | No binary download at install | The binary downloads on first use. Tests resolve it with `require("electron")`. |
| 41 | PDFs render in the same WebContents (OOPIF) | Verified: PDF view in the desktop app. |
| 20 | Renderer sandbox on by default | Already on. The preload script uses only `electron` modules. |
| 44 | `clipboard` removed from renderers | Not used in the renderer. |
| 44 | macOS 13 or later | The app requires macOS 13+. |

Not affected: `crashed` events (not used), `new-window` (not used),
`ipcRenderer.sendTo` (not used), protocol registration (not used).

## Other changes

- A latent bug became visible on Node.js 24: PDF text extraction passed the
whole pooled `Buffer.buffer` to pdf.js. It now copies the file bytes.
- electron-builder no longer rebuilds native modules (`npmRebuild: false`).
No runtime native module needs it: `fsevents` uses Node-API, and
`canvas` is not built or used.
- Removed the root `postinstall` key. It was outside `scripts`, so it never
ran, and its rebuild step is no longer needed.
- The bundled Node.js 24.21.0 for the storage service stays. The service
must run without Electron.

## Removed dependencies

Each one had no source, script, or configuration reference, or its only
use was removed. Clean install, builds, and all tests pass without them.

| Package | Reason |
| --- | --- |
| `@adobe/css-tools`, `ejs`, `follow-redirects`, `got`, `minimist`, `multer`, `node-fetch`, `node-forge`, `plist`, `postcss`, `terser`, `url` | No direct use. Packages that other packages need stay as transitive dependencies. `url` was shadowed by the Node.js built-in. |
| `@types/ejs`, `@types/follow-redirects`, `@types/minimist`, `@types/multer`, `@types/node-forge`, `@types/plist`, `@types/prettier`, `@types/eslint` | Types for packages that the code does not use. |
| `loader-utils`, `worker-loader` | Not used by any build. |
| `node-loader` and its webpack rule | Only loaded the old bundled `canvas` binary. |

Kept: `@angular/cli` and `@angular-devkit/architect` at the root (part of
the Angular upgrade), `electron-notarize` (used by `Resources/notarize.js`;
replace with `@electron/notarize` when signing is in scope),
`standard-version` (used by `yarn release`).

## Verified (2026-10-07, macOS 26.6 arm64)

| Command | Result |
| --- | --- |
| `yarn install --immutable` (clean copy) | Pass |
| `node -e 'require("electron")'` (clean copy) | Pass: v44.6.0 binary downloaded and checked |
| `yarn build-dev` | Pass |
| `yarn test` | 8 of 8 pass |
| `yarn test-storage` | 28 of 28 pass |
| `yarn workspace kc_storage typecheck` | Pass |
| `yarn e2e` | 3 of 3 pass: embedded browser (new), instance isolation, library workflow |
| `yarn e2e-browser` | 1 of 1 pass |
| `yarn package-local` | Pass: Electron 44.6.0, no native rebuild, unsigned |
| `yarn e2e-packaged` | 1 of 1 pass: copy outside the repository, scratch profiles, `PATH` without Node.js, bundled runtime |

Known warning: the main process prints a Node.js `punycode` deprecation
warning. It comes from a bundled third-party package, not from app code.
80 changes: 78 additions & 2 deletions docs/follow-ups.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Follow-up Work

Work found during the recovery, storage, and desktop completion
Work found during the recovery, storage, desktop, browser, and Electron
milestones. Not started.

## Storage service
Expand Down Expand Up @@ -28,7 +28,8 @@ milestones. Not started.

| Item | Reason |
| --- | --- |
| Upgrade Electron (26 is end-of-life) | Replace `File.path` with `webUtils.getPathForFile` (removed in Electron 32). |
| Keep Electron supported | Electron 44 support ends 2027-03-02. Move to the next supported major before then. |
| `punycode` deprecation warning | Comes from a bundled third-party package in the main process (probably the OpenAI SDK's older HTTP stack). |
| Upgrade Angular 14 and TypeScript 4.8 | Node.js 24 is outside Angular 14's supported range. |
| Bind the browser extension server to loopback with a token | It listens on port 9000 on all interfaces when enabled. |
| Replace the fixed API key passphrase | The key file is obfuscated, not encrypted. |
Expand All @@ -51,3 +52,78 @@ milestones. Not started.
- Full-text search over extracted text.
- Notes editor for the existing `notes` field.
- Verification of AI chat with a real API key.

## Recommendations for the next milestones

### 1. Upgrade Angular, TypeScript, and UI libraries

| Item | Now | Target | Notes |
| --- | --- | --- | --- |
| Angular (core, CLI, CDK, youtube-player) | 14.2 | 22.x (supported: 20, 21, 22) | Use `ng update` one major at a time (14 → 15 → … → 22). Run the builds and all end-to-end tests after each step. |
| TypeScript | 4.8 | 6.0 for Angular 22 (5.9 for 21) | The storage service already uses TypeScript 5.9 separately. |
| Node.js | 24.21 | 24.21 | Angular 22 supports `^24.15.0`. No change. |
| RxJS | 7.5 / 7.8 | 7.x | Compatible with all targets. |
| PrimeNG, PrimeFlex, PrimeIcons | 14 | Version that matches the Angular major | PrimeNG 17 and later change theming (styled mode and `@primeng/themes`). The theme files in `src/styles/themes` will need replacement. Highest effort. |
| FullCalendar Angular | 5 | 6 | API and CSS import changes. |
| chart.js, marked, cytoscape | 3, 4, 3 | Current | Independent of Angular. Upgrade after Angular. |
| `@angular-eslint`, `@typescript-eslint` | 14, 5 | Matching versions | Lint only. |

Affected areas: every component template that uses PrimeNG, the theme
service, the build configuration (`angular.json` moves to the application
builder), and `tsconfig.json`. The capability interfaces in `platform/` and
the browser and desktop tests protect the library workflow during the work.

### 2. Remove additional dependencies

- `axios` in the chat server: Node.js 24 has `fetch`.
- `openai` 4.25: update or replace; it is the likely source of the
`punycode` warning.
- `electron-notarize`: replace with `@electron/notarize` when signing is in
scope.
- Root `@angular/cli` and `@angular-devkit/architect`: remove during the
Angular upgrade (the Angular workspace has its own).
- `chokidar`, `cors`, `express`, `express-rate-limit`, `rxjs` in
`kc_electron`: reduce when chat moves to the backend.

### 3. Move chat and extraction into the backend

- Move `kc_electron/src/local` (chat routes, controllers, tokenizer, OpenAI
calls, `SourceParser`, `SourceLoader`) into `kc_storage` behind the same
token and session checks.
- Keep the API key in the library data directory, not in the profile
`data/` folder. Do not include it in backups.
- Move website fetching and metadata extraction to the backend. Then the
browser client can save websites. Apply an allow-list and size and time
limits to outgoing requests (server-side request forgery risk).
- Move the extracted-text cache from `data/storage/sources/` into the
library. This is also the base for full-text search later.
- Affected: `ChatService`, `ExtractorService`, `KsFactoryService` (website
path), `BackendService` (chat endpoint), the Electron chat server, and the
`tiktoken` resource.

### 4. Move durable user data out of renderer storage

| Data | Now | Target |
| --- | --- | --- |
| Chat history (`chat-<id>`) | Renderer local storage | Library (with chat) |
| Inbox (`ingest-queue`) | Renderer local storage | Library: inbox sources without a project |
| Topics list, favicon cache | Renderer local storage | Library, or derive again |
| Settings | Electron file (desktop), local storage (browser) | Library settings for user data; platform settings stay local |
| UI preferences (table, theme) | Renderer local storage | Can stay local |

After this, one library backup can hold all user data.

### 5. Separate local deployment from future hosted deployment

- Keep two modes with separate entry points. Local: loopback, bearer
token, launch-link sessions, one OS user (current). Hosted: accounts,
TLS, `Secure` cookies, server-side authorization per library, rate
limits, and audit logs.
- Do not reuse the local launch-link session as hosted authentication.
- Make the storage layer support more than one library per service before
hosting.
- Managed files need an object-storage option for hosting. The `AssetStore`
interface is the place to add it.
- Website fetching in a hosted service needs strict outgoing-request
controls.

119 changes: 119 additions & 0 deletions e2e/embedded-browser.e2e.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Desktop embedded browser (WebContentsView since the Electron 44 upgrade):
* open a website source in the Browser tab, check the view, its URL,
* resizing, and navigation state, then close it.
*
* Run: yarn build-dev && yarn e2e
*/

import { test } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import { close, launch, newRun, shot, startFixtureSite } from "./lib.mjs";
import * as ui from "./steps.mjs";

const SITE_TITLE = "Knowledge Fixture Site";

/** Child views of the main window, with their URLs and bounds. */
function views(app) {
return app.evaluate(({ BrowserWindow }) => {
const win = BrowserWindow.getAllWindows()[0];
return win.contentView.children.map((v) => ({
url: v.webContents?.getURL(),
bounds: v.getBounds(),
}));
});
}

async function waitFor(fn, check, timeout = 15000) {
const end = Date.now() + timeout;
let value;
while (Date.now() < end) {
value = await fn();
if (check(value)) return value;
await new Promise((r) => setTimeout(r, 250));
}
return value;
}

test(
"embedded browser opens, resizes, navigates, and closes",
{ timeout: 180000 },
async () => {
const run = newRun("embedded-browser");
const site = await startFixtureSite();
const ctx = await launch(
path.join(run, "profile"),
path.join(run, "app.log")
);
try {
await ui.createProject(ctx.page, "Browser View Project");
await ui.addLinkToProject(ctx.page, site.url, SITE_TITLE);
await ui.openTable(ctx.page);
await ui.openSource(ctx.page, SITE_TITLE);
await ctx.page.getByText("Browser", { exact: true }).click();

const open = await waitFor(
() => views(ctx.app),
(v) => v.length === 1 && v[0].url === site.url
);
assert.equal(open.length, 1, JSON.stringify(open));
assert.equal(open[0].url, site.url);
await ctx.page.waitForTimeout(1000);

// The view follows the window size. The app may replace the view with
// a new one at the new size; wait for one view again.
const before = open[0].bounds;
await ctx.app.evaluate(({ BrowserWindow }) => {
const win = BrowserWindow.getAllWindows()[0];
const [w, h] = win.getContentSize();
win.setContentSize(w - 100, h - 50);
});
await ctx.page.waitForTimeout(1500);
const after = await waitFor(
() => views(ctx.app),
(v) =>
v.length === 1 &&
v[0].bounds.width < before.width &&
v[0].url === site.url
);
assert.equal(after.length, 1, JSON.stringify(after));
assert.ok(
after[0].bounds.width < before.width,
JSON.stringify({ before, after })
);
assert.ok(
after[0].bounds.height < before.height,
JSON.stringify({ before, after })
);

// Navigation state through navigationHistory
const history = await waitFor(
() =>
ctx.app.evaluate(({ BrowserWindow }) => {
const view =
BrowserWindow.getAllWindows()[0].contentView.children[0];
if (!view) return null;
return {
back: view.webContents.navigationHistory.canGoBack(),
forward: view.webContents.navigationHistory.canGoForward(),
};
}),
(h) => h !== null
);
assert.deepEqual(history, { back: false, forward: false });
await shot(ctx.page, run, "browser-tab");

// Closing the source dialog removes the view
await ui.closeDialog(ctx.page);
const closed = await waitFor(
() => views(ctx.app),
(v) => v.length === 0
);
assert.deepEqual(closed, []);
} finally {
await close(ctx);
site.close();
}
}
);
10 changes: 6 additions & 4 deletions e2e/lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import { _electron as electron } from "playwright-core";
import fs from "node:fs";
import http from "node:http";
import path from "node:path";
import { createRequire } from "node:module";

export const REPO = path.resolve(import.meta.dirname, "..");
export const FIXTURES = path.join(REPO, "e2e", "fixtures");

export const ELECTRON = path.join(
REPO,
"node_modules/electron/dist/Electron.app/Contents/MacOS/Electron"
);
/**
* The Electron binary. Since Electron 42 the package downloads it on first
* use (checked against the package's checksums.json), not at install.
*/
export const ELECTRON = createRequire(import.meta.url)("electron");

/**
* Environment for the app. The Node test runner sets NODE_TEST_CONTEXT in
Expand Down
Loading