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
102 changes: 77 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,60 @@
# Cortex

Cortex is a local-first AI workspace for Windows. It runs Ollama models on the
machine, keeps conversations and memory in local storage, and presents the
React/TypeScript interface inside a Python-owned pywebview/WebView2 window.
The normal launcher owns the backend, native window, and any development
Cortex is a local-first AI workspace for Windows. It runs models on the machine
-- either through Ollama or by loading a local `.gguf` file with its own managed
llama.cpp runtime -- keeps conversations and memory in local storage, and
presents the React/TypeScript interface inside a Python-owned pywebview/WebView2
window. The normal launcher owns the backend, native window, and any development
frontend process; Cortex does not open the user's installed browser.

## Current workspace

The interface is deliberately small: a thread list, a focused transcript, a
The interface is deliberately small: a chat library, a focused transcript, a
composer with a local model picker, and settings for model, memory, appearance,
and execution controls.

Screenshots are intentionally omitted while the curated capture set is being
finalized. The examples used for the next capture pass are staged locally in an
isolated profile and are not part of the repository.
![The Cortex workspace: chat library with groups on the left, a transcript with
rendered Markdown and a syntax-highlighted code block, per-message copy,
regenerate, and fork controls, and the composer model picker
below.](docs/images/workspace.png)

Every response carries its own footer -- timestamp, token count, and tokens per
second -- with copy, regenerate, and fork controls that appear on hover.

Settings keep model selection and generation defaults in one place, and the chat
model doubles as the title model so there is only one choice to make:

![Cortex settings, AI Model section: top-p, top-k, and repeat penalty sliders,
context window and seed fields, a system instructions box, and a toggle to
bypass Cortex's default system prompt.](docs/images/settings.png)

`Ctrl`/`Cmd`+`K` opens a command palette that reaches new chat, settings, theme,
model switching, and recent conversations without leaving the keyboard:

![The Cortex command palette open over the workspace, listing chat actions,
installed models to switch to, and recent
chats.](docs/images/command-palette.png)

> The application in these images is the real build. The conversations are a
> fixed demo workspace defined in `tools/screenshots/demo_server.py` -- no model
> is contacted during a capture, so the images can be regenerated byte for byte
> with `./tools/screenshots/capture.ps1`.

## What Cortex provides

- **Local chat.** Stream responses from an installed Ollama model with Markdown,
- **Local chat.** Stream responses from a local model with Markdown,
syntax-highlighted fenced code, reasoning details, sources, code-block copy
controls, retry/regenerate, and forked threads. Long transcripts render in a
virtualized list so scroll performance stays smooth regardless of history
length.
- **Two local runtimes.** Use models installed through Ollama, or point Cortex at
a folder of `.gguf` files and it serves them through its own managed llama.cpp
runtime -- no separate install. The `llama-server` binary is fetched once,
verified against a pinned SHA-256, and cached. GPU backend selection is
`auto` (try Vulkan, fall back to CPU), `vulkan`, or `cpu`.
- **Bring your own GGUF.** Download a model into the local folder by direct URL
or Hugging Face repo, then select it from the same picker as everything else.
Local files appear as `gguf:<filename>`.
- **Composer model control.** Inspect the local inventory, switch models without
leaving the composer, refresh the inventory, and stage local image or text
attachments.
Expand All @@ -31,6 +63,10 @@ isolated profile and are not part of the repository.
for a single conversation from the composer, without changing the standing
default. Each response shows its token count and tokens/sec once generation
finishes.
- **Prompt control.** Standing system instructions apply to every turn, with no
length cap. A local model can also be run raw: **Bypass Cortex's default
system prompt** leaves the built-in identity and safety instructions out of
the request entirely. It is off by default and takes a deliberate opt-in.
- **Model details.** The Models panel shows each installed model's parameter
size, quantization, and context length alongside its name, read from Ollama's
existing model-detail response.
Expand Down Expand Up @@ -83,7 +119,7 @@ main.py
+-- supervised FastAPI backend (Python)
| +-- versioned loopback API, session auth, SSE jobs
| +-- SQLite conversations/settings and local memory repositories
| +-- Ollama/model and generation services
| +-- model and generation services (Ollama + managed llama.cpp)
| `-- scratch, image, attachment, and code execution lifecycles
+-- native pywebview / WebView2 window
`-- supervised Vite server (development mode only)
Expand All @@ -93,6 +129,7 @@ backend/cortex_backend/ API, repositories, services, and worker boundaries
contracts/ generated TypeScript API contracts
assets/ externalized model prompt assets
packaging/ Windows PyInstaller build and WebView2 bootstrapper
tools/ contract generation, qualification spikes, screenshots
tests/ Python API, lifecycle, worker, and migration tests
```

Expand All @@ -105,14 +142,16 @@ end to end; Cortex does not pull an embedding model at startup.

- Windows 10 or later
- Python 3.10 or later
- Ollama installed and running at `http://127.0.0.1:11434`
- At least one locally installed generation model
- At least one local model, from either runtime:
- Ollama installed and running at `http://127.0.0.1:11434`, or
- a `.gguf` file in the local models folder, served by the managed llama.cpp
runtime (no Ollama required)
- Node.js 22+ and npm for frontend development or source builds

## Quick start

Install a model in Ollama, create a virtual environment, and launch the native
desktop application:
Get a model, create a virtual environment, and launch the native desktop
application:

```powershell
ollama pull qwen3:8b
Expand All @@ -122,10 +161,15 @@ python -m pip install -r requirements.txt
python main.py
```

If you would rather not run Ollama, skip the first line and instead drop a
`.gguf` file into the local models folder shown under **Settings -> System**, or
download one from there by URL or Hugging Face repo. Cortex serves it with its
own llama.cpp runtime.

The first launch checks the local model inventory. Choose a model from the setup
screen or later from the composer picker. If Ollama is unavailable, Cortex still
opens and explains the connection state; generation becomes available after the
service is running and the inventory is refreshed.
screen or later from the composer picker. If neither runtime has a usable model,
Cortex still opens and explains the connection state; generation becomes
available once a model is present and the inventory is refreshed.

Useful launcher options:

Expand All @@ -143,17 +187,17 @@ endpoint can be intentionally changed for a trusted local network setup with

## Development checks

From the repository root:
One script runs the same gates CI runs, from the repository root:

```powershell
python -m pytest -q
python -m compileall -q main.py backend
./scripts/check.ps1 # lint, tests, contracts, frontend -- about two minutes
./scripts/check.ps1 -Tier full # adds compileall, Playwright, and the bundle build
```

npm.cmd ci --prefix frontend
npm.cmd run lint --prefix frontend
npm.cmd run typecheck --prefix frontend
npm.cmd test --prefix frontend -- --run
npm.cmd run build --prefix frontend
To run them automatically before every push:

```powershell
git config core.hooksPath .githooks
```

Use `npm.cmd` on Windows when PowerShell execution policy blocks the `npm.ps1`
Expand All @@ -163,6 +207,14 @@ shim. API contract artifacts are generated with:
python tools/generate_contracts.py
```

The README screenshots are regenerated from a fixed demo workspace, so they stay
in step with the UI without hand-editing images:

```powershell
npm.cmd run build --prefix frontend
./tools/screenshots/capture.ps1
```

## Windows packaging

Build the one-folder package with:
Expand Down
Binary file added docs/images/command-palette.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/workspace.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
124 changes: 124 additions & 0 deletions tools/screenshots/capture.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Capture the documentation screenshots against the staged demo workspace.
*
* Start tools/screenshots/demo_server.py first, then:
*
* node tools/screenshots/capture.mjs <bootstrap-token> [--port 8799]
*
* Everything is fixed -- viewport, workspace content, and the order of
* interactions -- so a re-run overwrites the images with the same pixels.
* Run from the frontend/ directory so the local playwright install resolves.
*/
import { mkdirSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, "..", "..");
const outDir = resolve(repoRoot, "docs", "images");

// Playwright lives in frontend/node_modules, not next to this script, so
// resolve it from there rather than relying on the caller's directory.
const requireFromFrontend = createRequire(resolve(repoRoot, "frontend", "package.json"));
const { chromium } = requireFromFrontend("@playwright/test");

const token = process.argv[2];
if (!token) {
console.error("usage: node capture.mjs <bootstrap-token> [--port N]");
process.exit(1);
}
const portFlag = process.argv.indexOf("--port");
const port = portFlag === -1 ? 8799 : Number(process.argv[portFlag + 1]);
const base = `http://127.0.0.1:${port}`;

const VIEWPORT = { width: 1440, height: 900 };

mkdirSync(outDir, { recursive: true });

const browser = await chromium.launch();
const page = await browser.newPage({
viewport: VIEWPORT,
deviceScaleFactor: 2, // crisp on high-DPI displays
colorScheme: "dark",
// Message times are rendered with Intl in the viewer's locale and zone.
// Pin both, or the captured footers differ from machine to machine.
locale: "en-US",
timezoneId: "UTC",
});

const shot = async (name) => {
const path = resolve(outDir, `${name}.png`);
await page.screenshot({ path });
console.log(` wrote docs/images/${name}.png`);
};

// Freeze anything time-based so repeated runs stay identical.
await page.addInitScript(() => {
const fixed = new Date("2026-08-09T17:00:00Z").valueOf();
const OriginalDate = Date;
// eslint-disable-next-line no-global-assign
Date = class extends OriginalDate {
constructor(...args) {
super(...(args.length ? args : [fixed]));
}
static now() {
return fixed;
}
};
});

console.log("Capturing...");

// --- 1. Workspace: sidebar library + a real transcript -----------------------
await page.goto(`${base}/?bootstrap=${encodeURIComponent(token)}`);
await page.getByLabel("Message Cortex").waitFor({ state: "visible" });

// The sidebar is already expanded at this width, so open the hero
// conversation straight from the library.
await page
.getByRole("button", { name: "Reading a 4 GB CSV without exhausting memory", exact: true })
.click();
await page.getByText("Read it as a").first().waitFor({ state: "visible" });
await page.waitForTimeout(600); // let syntax highlighting settle

// Frame the shot on the final exchange: put the closing question at the top so
// the whole answer, its footer, and the composer all land in one viewport.
await page.evaluate(() => {
const asked = document.querySelectorAll(".message-user");
asked[asked.length - 1]?.scrollIntoView({ block: "start" });
});
await page.waitForTimeout(900); // virtualized list re-renders the window

// The per-message controls (copy, regenerate, fork) are opacity:0 until the
// card is hovered or focused, so a plain screenshot would omit them entirely.
// Hover the final answer -- the only one where regenerate is enabled.
await page.locator(".message-assistant").last().hover();
await page.waitForTimeout(400); // 140ms opacity transition
await shot("workspace");

// --- 2. Settings: model + generation controls --------------------------------
await page.goto(`${base}/settings`);
await page.getByRole("heading", { name: "Settings", exact: true }).first().waitFor();
await page.getByRole("button", { name: "AI Model" }).click();
await page.getByLabel("System instructions").waitFor({ state: "visible" });

// The panel is taller than the viewport. Model selection is already visible in
// the composer of the workspace shot, so frame this one on the lower half --
// the generation parameters, system instructions, and the system-prompt bypass.
await page.evaluate(() => {
const pane = document.querySelector(".settings-pane");
if (pane) pane.scrollTop = pane.scrollHeight;
});
await page.waitForTimeout(400);
await shot("settings");

// --- 3. Command palette over the workspace -----------------------------------
await page.goto(`${base}/`);
await page.getByLabel("Message Cortex").waitFor({ state: "visible" });
await page.keyboard.press("Control+k");
await page.waitForTimeout(500);
await shot("command-palette");

await browser.close();
console.log("Done.");
66 changes: 66 additions & 0 deletions tools/screenshots/capture.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<#
.SYNOPSIS
Regenerate the README screenshots from the staged demo workspace.

.DESCRIPTION
Starts tools/screenshots/demo_server.py on an isolated port, hands the
one-time bootstrap token to the Playwright capture script, writes the
images to docs/images/, and stops the server again.

The workspace is fixture data and no model is contacted, so re-running this
reproduces the same images. Requires a built frontend bundle:

npm run build --prefix frontend

.EXAMPLE
./tools/screenshots/capture.ps1
#>
[CmdletBinding()]
param(
[int]$Port = 8799
)

$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)

if (-not (Test-Path (Join-Path $repoRoot 'frontend/dist/index.html'))) {
throw "No built frontend. Run: npm run build --prefix frontend"
}

$server = $null
try {
Write-Host "Starting staged demo server on port $Port..." -ForegroundColor Cyan

$stdout = New-TemporaryFile
$stderr = New-TemporaryFile
$server = Start-Process -FilePath 'python' `
-ArgumentList @('tools/screenshots/demo_server.py', '--port', $Port) `
-WorkingDirectory $repoRoot `
-RedirectStandardOutput $stdout `
-RedirectStandardError $stderr `
-NoNewWindow -PassThru

# The bootstrap token is the first line the server prints.
$token = $null
foreach ($attempt in 1..40) {
Start-Sleep -Milliseconds 250
$line = (Get-Content $stdout -TotalCount 1 -ErrorAction SilentlyContinue)
if ($line) { $token = $line.Trim(); break }
if ($server.HasExited) {
Get-Content $stderr | Write-Host -ForegroundColor Red
throw "Demo server exited before printing a token."
}
}
if (-not $token) { throw "Timed out waiting for the demo server bootstrap token." }

Write-Host 'Capturing screenshots...' -ForegroundColor Cyan
node (Join-Path $PSScriptRoot 'capture.mjs') $token --port $Port
if ($LASTEXITCODE -ne 0) { throw "Capture failed with exit code $LASTEXITCODE." }

Write-Host 'Screenshots written to docs/images/.' -ForegroundColor Green
} finally {
if ($server -and -not $server.HasExited) {
Stop-Process -Id $server.Id -Force -ErrorAction SilentlyContinue
Write-Host 'Demo server stopped.'
}
}
Loading
Loading