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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ jobs:
node-version: 22
cache: npm
- run: npm ci
# LibreOffice, so the test that turns Word and PowerPoint files into pages runs (it's skipped where it's missing).
- if: runner.os == 'Linux'
run: sudo apt-get update -qq && sudo apt-get install -y -qq --no-install-recommends libreoffice-writer-nogui libreoffice-impress-nogui
- run: npm run typecheck
- run: npm test

Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Search results are grouped by document. A PDF that matches on several pages is one result with its best passage, links to the other pages ("Also on p. 2, p. 7") and its other passages a click away; the heading counts documents and passages. Quick search shows one row per document.
- Open shows the document inside Duct, on its own, with your notebooks beside it; **Open in app** hands it to the program it belongs to. Closing one of two documents in the workspace leaves the other full width, and **One / Side by side / Stacked** switches between them.
- Selected text can go to any notebook: the pop-up says **Add to "<notebook>"**, and its menu lists every notebook you can add to, or starts a new one named after the document.
- Find in every document: the words you searched for are highlighted when it opens, "2 of 5" with previous and next (Shift+Enter, Enter) steps through every match on every page, and the find box takes other words too.
- Documents look like themselves: Word files with their headings, lists, tables and pictures; spreadsheets as tables with a tab per sheet; Markdown formatted; emails with their headers and body. Where LibreOffice is installed, Word, OpenDocument, RTF, PowerPoint and Keynote files are shown as their real pages instead (converted on this computer, once per version). **Text** still shows what Duct read. HTML from documents is cleaned to a short list of safe tags, and an email's remote images are never loaded. `GET /api/document-view`, `GET /api/document-pdf`.
- Search inside audio: voice notes (including WhatsApp's), recorded calls, meetings and interviews. What's said is transcribed on this computer by Whisper, never uploaded; the 77 MB model downloads once, the first time, and the privacy ledger lists it under On-device models. Results say "at 2:00" and play from there; the workspace shows the recording with its transcript, a time on every line. Off until turned on in Settings › Features; songs (tagged with an artist and an album) are skipped. Not available on Intel Macs.
- **Open at login** (Settings › This computer) starts Duct when you sign in, so quick search and watched folders are always ready.

### Fixed
- Long file names in All documents ran over the folder, status and buttons; they now end in "…", with the full name and path on hover.
- Highlights lit up a searched word inside another word ("tax" in "taxation").


## [1.0.0-alpha.2] - 2026-10-09

### Fixed
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ npx electron-builder --mac dir -c.mac.identity=null # unsigned packaged app in r

When launching Electron from a VS Code terminal, unset `ELECTRON_RUN_AS_NODE` first (`env -u ELECTRON_RUN_AS_NODE npm run electron:start`). Otherwise Electron runs as plain Node and `require('electron')` fails.

CI (`.github/workflows/ci.yml`) runs typecheck and tests on Linux, macOS and Windows, plus a build. Publishing to npm and GHCR happens on a GitHub release (`publish.yml`). Desktop installers aren't built in CI yet: signing and notarization need Apple and Windows certificates.
CI (`.github/workflows/ci.yml`) runs typecheck and tests on Linux, macOS and Windows, plus a build. Publishing to npm and GHCR happens on a GitHub release (`publish.yml`). Desktop installers are built on each release by `installers.yml` (unsigned until there are Apple and Windows certificates). `scripts/check-runtime-deps.mjs` fails a build whose code imports a package that isn't in `dependencies`.

## Layout

```
src/
index.ts Duct class: the whole pipeline, change detection, config, watched folders, diff, schema extraction
document-view.ts How the workspace shows non-PDF files: LibreOffice pages if installed, else cleaned HTML per format (cleanHtml allowlist), else text
library.ts addToLibrary(): copy a file into the Library (~/Duct Library), dedupe by hash, index it
version.ts VERSION read from package.json
types.ts All public interfaces (DuctConfig, RuntimeConfig, Chunk, Searcher, VectorStore, LLMProvider, …)
Expand Down
13 changes: 12 additions & 1 deletion assets/ui/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ body.no-mascot .mascot-slot { display: none; }
.type.slide { background: #2d1d0b; color: #ffb74d; }
.type.mail { background: #22163a; color: #b388ff; }
.type.img { background: #0e2830; color: #64d8e8; }
.type.audio { background: #2b1d10; color: #fbbf74; }
.type.zip { background: #262613; color: #e6e675; }

/* ---------- results ---------- */
Expand All @@ -163,7 +164,17 @@ body.no-mascot .mascot-slot { display: none; }
.result.selected { background: var(--s1); border-color: var(--border2); }
.result-main { flex: 1; min-width: 0; }
.result-title { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.result-title .name { font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; }
.result-title .name { font-weight: 600; color: var(--text); overflow: hidden; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.also-on { margin-top: 6px; font-size: 12.5px; color: var(--subtle); }
.also-on .link-btn { font-family: var(--mono); font-size: 11.5px; color: var(--lime); }
.more-btn { font: inherit; font-size: 12px; color: var(--subtle); background: none; border: 0; padding: 0; margin-top: 8px; cursor: pointer; text-decoration: underline dotted; text-underline-offset: 2px; }
.more-btn:hover, .more-btn:focus-visible { color: var(--text); }
.more-btn[aria-expanded="true"]::after { content: ' ▴'; }
.more-btn[aria-expanded="false"]::after { content: ' ▾'; }
.more-passages { list-style: none; margin: 8px 0 0; padding: 0 0 0 12px; border-left: 2px solid var(--border2); display: flex; flex-direction: column; gap: 10px; }
.more-passages li { cursor: pointer; }
.more-passages .snippet { margin-top: 2px; }
.more-passages .btn { margin-top: 4px; }
.where { font-family: var(--mono); font-size: 11px; color: var(--lime); }
.section { font-size: 12px; color: var(--subtle); }
.result .folder { font-family: var(--mono); font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 1px; }
Expand Down
Loading
Loading