# Project export: Noema — Your Work Remembers | Persistent AI Memory & Context

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Noema gives your work persistent intelligence—remembering context across sessions, grounding answers in your files, validating evidence, and securely sharing memory across the AI tools you use.
- Devpost: https://devpost.com/software/noema-your-work-remembers-persistent-ai-memory-context
- GitHub: https://github.com/tirthfx/noema
- Video: https://www.youtube.com/embed/JI51yO86MVg?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — aaryan1107 (1 commits), Codex (1 commits)

## Devpost submission (written by the team)

### Inspiration

We all forget the things we were working on. Important ideas get buried across Markdown files, meeting notes, browser tabs, project folders, and half-finished thoughts. Traditional chatbots can answer questions, but they do not truly understand the personal context behind a person’s work. Noema was inspired by the idea of a private, intelligent work companion: something that helps you recover your own thinking instead of constantly making you explain it again. The goal was to build a personal knowledge workspace that feels more like a thoughtful research partner than a search box.

### What it does

Noema gives users their own local knowledge corpus. It can: Search and answer questions from personal notes with verified citations Understand conversational context instead of relying on rigid keywords Ask the user to explicitly select a file or folder when more local context is needed Research the live web and provide source-grounded answers Turn notes into literature reviews and structured artifacts Capture URLs, text, and meeting transcripts into editable drafts Resurface forgotten ideas and open questions Maintain a local work timeline and focus memory Explain its approach through a concise, human-readable reasoning summary Noema never silently writes to the user’s corpus. Every proposed change must be reviewed and approved first.

### How we built it

Noema is a local-first Electron desktop application with a React and TypeScript interface. Its architecture is divided into: A local Markdown corpus owned by the user A lightweight semantic index for searching notes A main-process agent layer that communicates with NVIDIA NIM A secure preload bridge between Electron and the renderer A conversation-aware router that decides whether to answer directly, search the corpus, request selected local context, or research the web A citation validation layer that checks whether generated claims are actually supported by retrieved source text For local files and folders, Noema uses an explicit native picker. The user chooses what the assistant can inspect. Reads are bounded, credentials and .env files are excluded, and selected context is never silently added to the persistent corpus. For web research, Noema retrieves a bounded set of readable sources, sends the relevant text to the model, and only renders claims whose citations can be validated against the fetched source content.

### What we learned

The biggest lesson was that useful AI behavior is not just about generating better answers. It is about choosing the correct context. A greeting should not trigger a database search. A question about personal notes should not receive a generic answer. A request to inspect a local folder should not result in a fabricated response or unsafe filesystem access. We learned to treat routing, permissions, provenance, and user control as first-class parts of the AI experience. We also learned that trust comes from showing the user where an answer came from, what Noema did, and what it was unable to verify. Challenges we faced The hardest challenges were: Designing semantic routing without reducing the experience to brittle keyword rules Preventing hallucinated citations and unsupported corpus claims Safely handling local file and folder context in an Electron application Supporting live web research without turning the assistant into an unrestricted browser Preserving privacy while still making the product feel intelligent Creating a polished interface for complex agent states such as searching, requesting context, retrying, and asking for approval Making the entire flow work reliably during a hackathon demo The final result is a hackathon-safe personal knowledge companion that combines memory, research, and conversation while keeping the user in control.

## README (from the GitHub repository)

# Noema

Noema is a desktop knowledge workspace with its own local Markdown corpus. It turns notes, meetings, captures, and focus checkpoints into searchable working memory without treating model output as evidence. Existing folders and Obsidian vaults remain optional connectors.

## The problem

Researchers, educators, and students accumulate notes that are hard to revisit as a whole. Important connections and contradictions stay buried, and turning scattered reading notes into a reliable draft still takes manual work. Ordinary chat tools do not retain a user's corpus as persistent working knowledge, and they cannot safely act on it.

## The solution

Noema creates `Documents/Noema Library` as a transparent corpus of ordinary files, gives an agent bounded tools to search and read it, and turns retrieved notes into grounded answers and research artifacts. Every displayed citation is checked in code against the source note. Any proposed write stays editable and unwritten until the user explicitly approves it.

## Why Noema

1. **Code-validated citations, not model-made references.** A claim only renders with a citation after Noema verifies the cited text against the corpus; unsupported claims are removed.
2. **An agent that can act, behind an approval gate.** Capture, meeting, and linking flows produce editable proposals. Nothing reaches the corpus until the user chooses **Approve & write**.
3. **A daily knowledge loop.** Today suggests useful next prompts, Review turns resurfaced notes into recovery work, and every answer offers a concrete next action.
4. **Hackathon-safe Focus Memory.** A user explicitly selects one screen or window, adds checkpoints, and receives a local recap connected to related notes. No screen image or focus record is sent to NIM, and recap notes still require approval.
5. **Private continuity, without ambient surveillance.** Work Timeline uses real local file timestamps, meeting tasks can enter Review, and `Cmd/Ctrl + Shift + Space` reopens Ask from anywhere. Noema never starts background screen or audio capture.
6. **A useful assistant, not a compulsory search box.** Noema semantically interprets the conversation and chooses a direct response, selected local context, or citation-validated corpus retrieval. When a local file or folder is needed, it asks for explicit access and resumes the same request after selection.
7. **Live research with a visible approach.** Current or explicitly online questions trigger bounded web research, readable-source extraction, and code-validated clickable citations. Every answer can reveal a concise decision summary, and web sources can enter the existing editable Capture flow.

The interface is a light Reading Room: a calm, sidebar-based workspace where green means verified and amber appears only while the agent is actively using a tool.

## Demo video

A public demo-video link will be added here once the recording is uploaded. The recording plan is in [`docs/demo-script.md`](docs/demo-script.md).

## Built with

- **GPT-5.6 / Codex** was used to design, implement, test, and package the Electron application. Codex contributed the final submission pass.
- **Tirth Shendage** contributed the hackathon submission, product direction, and demo refinement.
- **NVIDIA NIM** runs the app at runtime: `meta/llama-3.1-8b-instruct` powers the agent and `nvidia/llama-nemotron-embed-1b-v2` provides corpus embeddings.

## Download and run

The [Package desktop apps workflow](https://github.com/tirthfx/noema/actions/workflows/package.yml) publishes unsigned desktop artifacts. Verified run `29519838958` produced:

- macOS: ARM64 and x64 `.dmg` and `.zip` packages.
- Windows: x64 NSIS installer and portable `.exe` packages.

### macOS

1. Download the artifact that matches your Mac (`arm64` for Apple Silicon; `x64` for Intel).
2. Open the `.dmg` and drag Noema to Applications, or unzip the `.zip`.
3. Because this hackathon build is unsigned, Control-click Noema, choose **Open**, then choose **Open** again in the confirmation dialog. You only need to do this the first time.
4. If macOS still blocks the app, open **System Settings → Privacy & Security** and choose **Open Anyway** for Noema.

### Windows

1. Download the x64 NSIS installer, or use the portable `.exe` if you do not want an installation.
2. Run the file. Windows SmartScreen may show a protection screen because the build is unsigned.
3. Choose **More info**, then **Run anyway**. The installer lets you select an installation folder.

## Before your first corpus index

Noema uses NVIDIA NIM for embeddings and synthesis. Set `NVIDIA_API_KEY` in your environment before launching the app. For development, you can also create a local `.env` file:

```bash
NVIDIA_API_KEY=your_key_here
```

The key stays in Electron's main process and is never exposed to the renderer.

> **Hackathon packaging risk:** current judge/demo installers deliberately bundle `.env` so
> they run with zero setup. Anyone who receives an installer can extract that NVIDIA key.
> Do not distribute these builds publicly; remove the `extraResources` entry and rotate the
> key before a public release.

## Development

```bash
npm ci
npm run dev
```

Useful checks:

```bash
npm run typecheck
npm run smoke:local
npm run smoke:workspace
npm run package:mac
```

Windows packages are built on the native Windows GitHub Actions runner; macOS packages are built on the native macOS runner. This avoids cross-platform packaging mismatches.

## MCP: let other agents query your corpus

Noema can expose its indexed corpus to other agents (Claude, Codex, etc.) over the Model Context Protocol, so an agent working on something else can pull grounded, cited context from your notes instead of guessing or making you re-explain it.

```bash
npm run mcp
```

This starts a read-only stdio MCP server with four tools: `search_notes`, `read_note`, `list_notes`, and `ask_corpus` (a fully synthesized, citation-validated answer — the same grounding pipeline as the in-app Ask). By default it reads whichever vault you last opened in the desktop app; point it at a specific vault instead with `NOEMA_VAULT_PATH=/path/to/vault`.

There is deliberately no `write_note` or `link_notes` tool here. Every write in Noema stays behind the in-app `EditablePreview` approval gate — an external agent can read and ask, but it cannot commit anything to disk without a human clicking **Approve & write** inside the app itself.

## Knowledge workspace UI

The app is a warm-paper, light-only workspace with six connected areas: Today, Ask, Create, Review, Capture, and Library. Green marks selected, verified, local, and approved states. Amber appears only while an agent operation is active.

### Focus Memory privacy boundary

- It never starts automatically.
- Screen or window selection is explicit through Noema's local Electron source picker.
- The current hackathon build stores user-entered session context, timestamps, and checkpoints locally.
- It does not claim continuous OCR, microphone capture, system-audio capture, or behavioral profiling.
- Saving a recap to the corpus opens the same editable approval gate as every other write.

### Selected context privacy boundary

- Typing a local path never grants filesystem access. Noema opens a native picker and reads only the file or folder the user explicitly selects.
- The renderer receives an opaque token, not authority to request arbitrary paths. Context tokens expire after four hours.
- Reads are text-only and bounded; dependency/build folders, hidden files, `.env` files, credentials, and private-key-like files are excluded.
- Selected text is sent to NVIDIA NIM to answer the current request. It is not imported into the persistent corpus unless the user separately approves a Capture write.


## Detected evidence (automated analysis)

Indexed codebase: 80 recognized source files, 424 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (87 of 87)

```
.github/workflows/package.yml
.gitignore
docs/architecture.md
docs/demo-script.md
docs/design.md
docs/memory.md
docs/phases.md
docs/prd.md
docs/project_status_compilation.md
docs/rules.md
docs/superpowers/plans/2026-07-19-noema-knowledge-workspace.md
docs/superpowers/specs/2026-07-19-noema-knowledge-workspace-design.md
electron-builder.yml
electron.vite.config.ts
electron/agent.ts
electron/capture.ts
electron/citation-validator.ts
electron/context-reader.ts
electron/corpus.ts
electron/index.ts
electron/ipc-handlers.ts
electron/live-context-check.ts
electron/main.ts
electron/mcp-server.ts
electron/model-json.ts
electron/phase0-smoke.ts
electron/phase1-smoke.ts
electron/phase10-smoke.ts
electron/phase11-smoke.ts
electron/phase12-smoke.ts
electron/phase3-smoke.ts
electron/phase4-smoke.ts
electron/phase5-smoke.ts
electron/phase6-smoke.ts
electron/phase9-smoke.ts
electron/preload.ts
electron/selected-context-index.ts
electron/tools/link-notes.ts
electron/tools/list-notes.ts
electron/tools/read-note.ts
electron/tools/search-notes.ts
electron/tools/write-note.ts
electron/vault-pointer.ts
electron/vault.ts
electron/web-research.ts
electron/workspace-store.ts
index.html
package.json
postcss.config.cjs
README.md
seed-vault/.noema/config.json
seed-vault/.noema/index.json
seed-vault/01-attention.md
seed-vault/02-retrieval.md
seed-vault/03-spacing.md
seed-vault/04-cramming.md
seed-vault/05-interleaving.md
seed-vault/06-feedback.md
seed-vault/07-sleep.md
seed-vault/08-walking.md
seed-vault/09-notes.md
seed-vault/10-questions.md
seed-vault/11-ambient.md
seed-vault/12-ritual.md
seed-vault/13-analogy.md
seed-vault/14-time.md
seed-vault/15-contradiction-long-term.md
seed-vault/interleaving-improves-strategy-selection.md
shared/types.ts
shared/workspace.ts
src/App.tsx
src/components/AnswerView.tsx
src/components/ArtifactView.tsx
src/components/Citation.tsx
src/components/CorpusOnboarding.tsx
src/components/CorpusOverview.tsx
src/components/EditablePreview.tsx
src/components/RecallCard.tsx
src/components/ToolCallIndicator.tsx
src/main.tsx
src/renderer/index.html
src/styles/app.css
src/styles/tokens.css
src/vite-env.d.ts
tailwind.config.ts
tsconfig.json
tsconfig.web.json
```

### Dependencies

- package.json: @electron-toolkit/tsconfig@^1.0.1, @fontsource-variable/geist@^5.3.0, @fontsource/jetbrains-mono@^5.2.7, @fontsource/source-serif-4@^5.2.7, @gsap/react@^2.1.2, @modelcontextprotocol/sdk@^1.29.0, @phosphor-icons/react@^2.1.10, @types/node@^22.15.3, @types/react@^19.1.2, @types/react-dom@^19.1.2, @vitejs/plugin-react@^4.4.1, autoprefixer@^10.4.21, electron@^43.1.1, electron-builder@^26.0.12, electron-vite@^3.1.0, gsap@^3.15.0, react@^19.1.1, react-dom@^19.1.1, tailwindcss@^3.4.17, typescript@^5.8.3, vite@^6.3.5, zod@^4.4.3

### Recent commits (newest first)

- Add Tirth submission attribution
- Add Codex submission attribution
- Submit Noema

## Key source files (fetched from GitHub, selected and truncated for size)

### seed-vault/12-ritual.md

```markdown
# Starting rituals

A repeated opening ritual can make the beginning of a study block less dependent on motivation.

```

### seed-vault/13-analogy.md

```markdown
# Analogies and transfer

An analogy is valuable when it clarifies structure without hiding the limits of the comparison.

```

### package.json

```
{
  "name": "noema",
  "version": "0.1.0",
  "private": true,
  "author": "Aaryan Kansal",
  "description": "A local-first personal knowledge corpus and research companion.",
  "type": "module",
  "main": "out/main/index.js",
  "scripts": {
    "dev": "electron-vite dev",
    "build": "npm run typecheck && electron-vite build && electron-builder",
    "build:unpack": "npm run typecheck && electron-vite build && electron-builder --dir",
    "package:mac": "npm run typecheck && electron-vite build && electron-builder --mac --arm64 --x64",
    "package:win": "npm run typecheck && electron-vite build && electron-builder --win --x64",
    "smoke:index": "electron-vite build && node out/main/phase1-smoke.js",
    "smoke:local": "electron-vite build && node out/main/phase0-smoke.js",
    "smoke:artifact": "electron-vite build && node out/main/phase3-smoke.js",
    "smoke:answer": "electron-vite build && node out/main/phase4-smoke.js",
    "smoke:capture": "electron-vite build && node out/main/phase5-smoke.js",
    "smoke:workspace": "electron-vite build && node out/main/phase6-smoke.js",
    "smoke:meeting": "electron-vite build && node out/main/phase9-smoke.js",
    "smoke:corpus": "electron-vite build && node out/main/phase10-smoke.js",
    "smoke:remediation": "electron-vite build && node out/main/phase11-smoke.js",
    "smoke:continuity": "electron-vite build && node out/main/phase12-smoke.js",
    "check:live-context": "electron-vite build && node out/main/live-context-check.js",
    "test": "npm run smoke:remediation && node out/main/phase12-smoke.js",
    "mcp": "electron-vite build && node out/main/mcp-server.js",
    "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.web.json"
  },
  "dependencies": {
    "@fontsource-variable/geist": "^5.3.0",
    "@fontsource/jetbrains-mono": "^5.2.7",
    "@fontsource/source-serif-4": "^5.2.7",
    "@gsap/react": "^2.1.2",
    "@modelcontextprotocol/sdk": "^1.29.0",
    "@phosphor-icons/react": "^2.1.10",
    "gsap": "^3.15.0",
    "react": "^19.1.1",
    "react-dom": "^19.1.1",
    "zod": "^4.4.3"
  },
  "devDependencies": {
    "@electron-toolkit/tsconfig": "^1.0.1",
    "@types/node": "^22.15.3",
    "@types/react": "^19.1.2",
    "@types/react-dom": "^19.1.2",
    "@vitejs/plugin-react": "^4.4.1",
    "autoprefixer": "^10.4.21",
    "electron": "^43.1.1",
    "electron-builder": "^26.0.12",
    "electron-vite": "^3.1.0",
    "tailwindcss": "^3.4.17",
    "typescript": "^5.8.3",
    "vite": "^6.3.5"
  }
}

```

### src/main.tsx

```typescript
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './styles/tokens.css'
import './styles/app.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

```

### electron/main.ts

```typescript
import { app, BrowserWindow, desktopCapturer, globalShortcut, ipcMain, session } from 'electron'
import { join } from 'node:path'
import { registerIpcHandlers } from './ipc-handlers'

let mainWindow: BrowserWindow | null = null
let pendingDisplaySourceId: string | null = null

function createWindow(): void {
  mainWindow = new BrowserWindow({
    width: 1120,
    height: 720,
    minWidth: 800,
    minHeight: 560,
    show: false,
    backgroundColor: '#F7F6F2',
    titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'hidden',
    frame: process.platform === 'win32' ? false : undefined,
    webPreferences: {
      preload: join(__dirname, '../preload/preload.cjs'),
      contextIsolation: true,
      nodeIntegration: false,
      sandbox: true,
      webSecurity: true
    }
  })

  mainWindow.once('ready-to-show', () => mainWindow?.show())
  mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))

  if (process.env.ELECTRON_RENDERER_URL) {
    void mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
  } else {
    void mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
  }
}

app.whenReady().then(() => {
  ipcMain.handle('focus:list-sources', async () => (await desktopCapturer.getSources({ types: ['window', 'screen'], fetchWindowIcons: false, thumbnailSize: { width: 0, height: 0 } })).map((source) => ({ id: source.id, name: source.name })))
  ipcMain.handle('focus:select-source', (_event, id: unknown) => { pendingDisplaySourceId = typeof id === 'string' ? id : null })
  session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => {
    const selectedId = pendingDisplaySourceId
    pendingDisplaySourceId = null
    if (!selectedId) return callback({})
    const sources = await desktopCapturer.getSources({ types: ['window', 'screen'], thumbnailSize: { width: 0, height: 0 } })
    const selected = sources.find((source) => source.id === selectedId)
    callback(selected ? { video: selected } : {})
  })
  registerIpcHandlers(() => mainWindow)
  createWindow()
  globalShortcut.register('CommandOrControl+Shift+Space', () => {
    if (!mainWindow || mainWindow.isDestroyed()) return
    if (mainWindow.isMinimized()) mainWindow.restore()
    mainWindow.show()
    mainWindow.focus()
    mainWindow.webContents.send('app:quick-ask')
  })
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('will-quit', () => globalShortcut.unregisterAll())

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit()
})

```

### electron/index.ts

```typescript
import { readFile, mkdir, rename, rm, writeFile } from 'node:fs/promises'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import type { CorpusNote, IndexProgress, IndexRecord, IndexStatus, RecallItem, SearchMatch } from '../shared/types'
import { basename } from 'node:path'
import { chunkMarkdown, readVaultNote, walkMarkdownFiles } from './vault'

export const EMBEDDING_MODEL = 'nvidia/llama-nemotron-embed-1b-v2'
export const EMBEDDING_DIMENSION = 2048
export const CHAT_MODEL = 'meta/llama-3.1-8b-instruct'
const NIM_BASE_URL = 'https://integrate.api.nvidia.com/v1'
const INDEX_VERSION = 1

interface StoredIndex {
  version: number
  model: string
  dimension: number
  records: IndexRecord[]
}

export class NimApiError extends Error {
  constructor(message: string, readonly status?: number) {
    super(message)
    this.name = 'NimApiError'
  }
}

/** Parses the NVIDIA key without treating a `#` inside quotes as an inline comment. */
export function parseNvidiaApiKeyEnv(source: string): string | undefined {
  for (const line of source.split(/\r?\n/)) {
    const match = line.match(/^\s*(?:export\s+)?NVIDIA_API_KEY\s*=\s*(.*)$/)
    if (!match) continue
    const raw = match[1].trim()
    if (!raw) return undefined
    if (raw[0] === '"' || raw[0] === "'") {
      const quote = raw[0]
      let escaped = false
      for (let index = 1; index < raw.length; index += 1) {
        const char = raw[index]
        if (quote === '"' && char === '\\' && !escaped) {
          escaped = true
          continue
        }
        if (char === quote && !escaped) return raw.slice(1, index)
        escaped = false
      }
      return undefined
    }
    // dotenv treats # as a comment delimiter for unquoted values; quote the value to keep it.
    const inlineComment = raw.indexOf('#')
    const value = (inlineComment === -1 ? raw : raw.slice(0, inlineComment)).trim()
    return value || undefined
  }
  return undefined
}

function readEnvKey(envFilePath: string): string | undefined {
  try {
    return parseNvidiaApiKeyEnv(readFileSync(envFilePath, 'utf8'))
  } catch {
    return undefined
  }
}

let fallbackApiKeyResolved = false
let fallbackApiKey: string | undefined

function readApiKey(): string {
  // Check the environment on every call so a deliberate runtime assignment still takes
  // precedence, while the synchronous bundled-file fallback is resolved at most once.
  const key = process.env.NVIDIA_API_KEY?.trim()
  if (key) return key
  // process.cwd() covers `npm run dev` (launched from the project root). A packaged,
  // double-clicked app has neither an inherited shell env nor a project-rooted cwd, so it
  // needs its own bundled .env — electron-builder copies one into Contents/Resources at
  // build time (see electron-builder.yml `extraResources`), reachable via resourcesPath.
  if (!fallbackApiKeyResolved) {
    const candidatePaths = [join(process.cwd(), '.env'), typeof process.resourcesPath === 'string' ? join(process.resourcesPath, '.env') : null].filter((path): path is string => path !== null)
    for (const path of candidatePaths) {
      fallbackApiKey = readEnvKey(path)
      if (fallbackApiKey) break
    }
    fallbackApiKeyResolved = true
  }
  if (fallbackApiKey) return fallbackApiKey
  throw new NimApiError('NVIDIA_API_KEY is not configured in the main process.')
}

/** Kept in the main process and shared by every NIM call site. */
export function readNimApiKey(): string {
  return readApiKey()
}

function retryDelay(response: Response): Promise<void> {
  const retryAfter = Number(response.headers.get('retry-after'))
  const delay = Number.isFinite(retryAfter) && retryAfter > 0
    ? Math.min(retryAfter * 1_000, 15_000)
    : 5_000
  return new Promise((resolve) => setTimeout(resolve, delay))
}

async function nimFetch(path: string, body: Record<string, unknown>): Promise<Response> {
  let lastError: Error | null = null
  for (let attempt = 0; attempt < 2; attempt += 1) {
    try {
      const response = await fetch(`${NIM_BASE_URL}${path}`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${readNimApiKey()}`, 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(45_000)
      })
      if (response.ok || (response.status < 500 && response.status !== 429)) return response
      lastError = new NimApiError(`NIM request failed with HTTP ${response.status}.`, response.status)
      if (attempt === 0) await retryDelay(response)
    } catch (error) {
      lastError = error instanceof Error && error.name === 'TimeoutError'
        ? new NimApiError('NIM request timed out.')
        : error instanceof Error ? error : new NimApiError('NIM request failed before a response was received.')
    }
  }
  throw new NimApiError(`NIM request failed after one retry: ${lastError?.message ?? 'unknown error'}`)
}

async function embed(text: string, inputType: 'passage' | 'query'): Promise<number[]> {
  const response = await nimFetch('/embeddings', {
    model: EMBEDDING_MODEL,
    input: text,
    input_type: inputType,
    encoding_format: 'float',
    truncate: 'NONE'
  })
  if (response.status === 403) {
    throw new NimApiError('NIM denied the embedding request. Check this API key has access to NVIDIA Public API Endpoints.', 403)
  }
  if (!response.ok) throw new NimApiError(`NIM embedding request failed with HTTP ${response.status}.`, response.status)
  const payload = await response.json() as { data?: Array<{ embedding?: unknown }> }
  const vector = payload.data?.[0]?.embedding
  if (!Array.isArray(vector) || vector.length !== EMBEDDING_DIMENSION || !vector.every((value) => typeof value === 'number')) {
    throw new NimApiError('NIM returned an invalid embedding vector.')
  }
  return vector
}

function cosineSimilarity(left: number[], right: number[]): number {
  let dot = 0
  let leftMagnitude = 0
  let rightMagnitude = 0
  for (let index = 0; index < left.length; index += 1) {
    dot += 
[truncated — 9140 more characters]
```

### src/App.tsx

```typescript
import { FormEvent, useEffect, useMemo, useRef, useState } from 'react'
import {
  Archive, Brain, ChatCircleDots, Check, ClockCounterClockwise, FilePlus, FileText, FolderOpen, House,
  Link as LinkIcon, ListChecks, MagnifyingGlass, MonitorPlay, Pause, Play, Plus, Sparkle,
  Stop, Trash, Waveform, X
} from '@phosphor-icons/react'
import type {
  ActivityEvent, AgentResult, Artifact, CaptureKind, ContextSelection, ConversationTurn, CorpusNote, DisplaySource, FocusSession, GroundedAnswer, IndexProgress,
  NoteProposal, NoteSummary, Persona, PersistedChatTurn, RecallItem, ReviewItem, SessionContinuity, SuggestedPrompt, ToolCallActivity, VaultSelection, WorkspaceSessionState, TimelineEvent
} from '../shared/types'
import { buildFocusRecap, buildMeetingReviewItems, buildReviewItems, buildSessionRecap, buildSuggestedPrompts } from '../shared/workspace'
import AnswerView from './components/AnswerView'
import ArtifactView from './components/ArtifactView'
import CorpusOverview from './components/CorpusOverview'
import CorpusOnboarding from './components/CorpusOnboarding'
import EditablePreview from './components/EditablePreview'
import ToolCallIndicator from './components/ToolCallIndicator'

type WorkspaceMode = 'today' | 'ask' | 'create' | 'review' | 'capture' | 'library'
type ChatEntry =
  | { kind: 'question'; id: string; text: string }
  | { kind: 'tool'; value: ToolCallActivity }
  | { kind: 'answer'; id: string; value: GroundedAnswer; question: string }
type ChatError = AgentResult & { prompt: string }

const NAV = [
  { id: 'today' as const, label: 'Today', icon: House },
  { id: 'ask' as const, label: 'Ask', icon: ChatCircleDots },
  { id: 'create' as const, label: 'Create', icon: Sparkle },
  { id: 'review' as const, label: 'Review', icon: ListChecks },
  { id: 'capture' as const, label: 'Capture', icon: FilePlus },
  { id: 'library' as const, label: 'Library', icon: Archive }
]

const TITLES: Record<WorkspaceMode, { eyebrow: string; title: string; description: string }> = {
  today: { eyebrow: 'DAILY ORIENTATION', title: 'What deserves your attention?', description: 'Continue a thread, close a knowledge gap, or recover something worth remembering.' },
  ask: { eyebrow: 'CONVERSATION + MEMORY', title: 'Ask Noema anything', description: 'Noema interprets the conversation, chooses the right evidence mode, and can research your corpus, selected local context, or the live web.' },
  create: { eyebrow: 'ARTIFACT STUDIO', title: 'Turn notes into an argument', description: 'Create a literature review that exposes evidence, tensions, and missing ground.' },
  review: { eyebrow: 'KNOWLEDGE RECOVERY', title: 'Strengthen what is fading', description: 'Review open loops from your own notes, then mark them resolved.' },
  capture: { eyebrow: 'INBOX TO KNOWLEDGE', title: 'Bring something into your corpus', description: 'Noema drafts and files it. Nothing is written until you approve the exact note.' },
  library: { eyebrow: 'SOURCE OF TRUTH', title: 'Your indexed library', description: 'Inspect what Noema can actually search, cite, and connect.' }
}

function answerText(answer: GroundedAnswer): string {
  return answer.plainText ?? answer.notice ?? answer.claims.map((claim) => claim.text).join('\n')
}

function chatHistory(entries: ChatEntry[]): ConversationTurn[] {
  return entries.flatMap((entry): ConversationTurn[] => {
    if (entry.kind === 'question') return [{ role: 'user', content: entry.text }]
    if (entry.kind !== 'answer') return []
    const content = answerText(entry.value)
    return content ? [{ role: 'assistant', content }] : []
  }).slice(-8)
}

/** Distil the live conversation into a bounded, serializable transcript for durable restore. */
function persistConversation(entries: ChatEntry[]): PersistedChatTurn[] {
  return entries.flatMap((entry): PersistedChatTurn[] => {
    if (entry.kind === 'question') return [{ role: 'user', content: entry.text }]
    if (entry.kind !== 'answer') return []
    const content = answerText(entry.value)
    return content ? [{ role: 'assistant', content, mode: entry.value.mode }] : []
  }).slice(-40)
}

/** Rebuild displayable chat entries from a restored transcript (tool logs are not persisted). */
function restoreConversation(turns: PersistedChatTurn[]): ChatEntry[] {
  return turns.map((turn): ChatEntry => turn.role === 'user'
    ? { kind: 'question', id: crypto.randomUUID(), text: turn.content }
    : { kind: 'answer', id: crypto.randomUUID(), question: '', value: { claims: [], mode: turn.mode ?? 'conversation', plainText: turn.content } })
}

const WORKSPACE_MODES = new Set<WorkspaceMode>(['today', 'ask', 'create', 'review', 'capture', 'library'])

function WindowsControls() {
  if (!navigator.userAgent.includes('Windows')) return null
  return <div className="window-controls" aria-label="Window controls">
    <button aria-label="Minimize window" onClick={() => void window.noema.window.minimize()}>−</button>
    <button aria-label="Maximize window" onClick={() => void window.noema.window.toggleMaximize()}>□</button>
    <button className="close-control" aria-label="Close window" onClick={() => void window.noema.window.close()}>×</button>
  </div>
}

function PromptSuggestions({ prompts, onChoose }: { prompts: SuggestedPrompt[]; onChoose: (prompt: string) => void }) {
  return <div className="prompt-stack">{prompts.map((item) => (
    <button className="prompt-row" key={item.id} onClick={() => onChoose(item.prompt)}>
      <span><strong>{item.label}</strong><small>{item.prompt}</small></span>
      <span className="prompt-reason">{item.reason}</span>
    </button>
  ))}</div>
}

function FocusPanel({
  sessions, recalls, onSave, onDelete, onProposal
}: {
  sessions: FocusSession[]
  recalls: RecallItem[]
  onSave: (session: FocusSession) => Promise<void>
  onDelete: (id: string) => Promise<void>
  onProposal: (session: FocusSession) => Promise<void>
}) {
  const [context, setContext] = useState('')
  const [checkpoint, setCheckpoint] = useState
[truncated — 37602 more characters]
```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'none'; base-uri 'self'; form-action 'none';" />
    <title>Noema</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### electron-builder.yml

```yaml
appId: com.noema.app
productName: Noema
directories:
  output: release
files:
  - out/**
  - package.json
extraResources:
  # ACCEPTED DEMO RISK: this makes the NVIDIA key extractable from every installer.
  # Keep only for zero-configuration judging; remove it and rotate the key before public release.
  - .env
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
mac:
  icon: build/icon.icns
  target:
    - target: dmg
      arch:
        - arm64
        - x64
    - target: zip
      arch:
        - arm64
        - x64
  identity: null
win:
  icon: build/icon.png
  target:
    - target: nsis
      arch:
        - x64
    - target: portable
      arch:
        - x64
nsis:
  oneClick: false
  allowToChangeInstallationDirectory: true

```

### tailwind.config.ts

```typescript
import type { Config } from 'tailwindcss'

export default {
  content: ['./index.html', './src/**/*.{ts,tsx}'],
  theme: {
    extend: {
      colors: {
        base: 'var(--bg-base)',
        surface: 'var(--bg-surface)',
        elevated: 'var(--bg-elevated)',
        inset: 'var(--bg-inset)',
        border: { subtle: 'var(--border-subtle)', strong: 'var(--border-strong)' },
        text: { primary: 'var(--text-primary)', secondary: 'var(--text-secondary)', tertiary: 'var(--text-tertiary)' },
        amber: { DEFAULT: 'var(--accent-amber)', dim: 'var(--accent-amber-dim)', bg: 'var(--accent-amber-bg)' },
        state: { success: 'var(--state-success)', error: 'var(--state-error)', warning: 'var(--state-warning)' }
      },
      fontFamily: {
        ui: 'var(--font-ui)',
        content: 'var(--font-content)',
        mono: 'var(--font-mono)'
      }
    }
  },
  plugins: []
} satisfies Config

```

[70 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]