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
4 changes: 4 additions & 0 deletions docs/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ changing a contract that other features depend on.
- [Journal](./journal.md) — daily automatic +
user journal entries, the journaling agent, the
`journal_*` tools, and the Journal modal.
- [Wiki](./wiki.md) — flat encyclopedic articles about
the user, the autonomous wiki agent, the per-article
manual update flow, the `wiki_*` tools, and the Wiki
drawer tab.
- [Cookbook](./cookbook.md) — `recipes` store + Cooklang
parser + the recipe_* tools + the Cookbook modal and
drawer tab.
Expand Down
399 changes: 399 additions & 0 deletions docs/dev/wiki.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions docs/user/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ You can reach these pages two ways:
- [Memory](./memory.md) — the long-term store Nak builds up about you
across conversations: what gets remembered, how to correct or
forget something, what's scoped to your account.
- [Wiki](./wiki.md) — a flat encyclopedia about you: titled articles
about projects, people, places, and topics, maintained by both you
and a background agent. The assistant reaches them through the
always-on `wiki_search` tool.
- [Intuition](./intuition.md) — the subconscious read Nak forms of
each conversation: how the brain icon next to the mood emoji works,
and what the inline cards mean.
Expand Down
161 changes: 161 additions & 0 deletions docs/user/wiki.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Wiki

The Wiki is a flat encyclopedia about you. Every entry is a titled
article in encyclopedic third-person prose - about a project, a person
in your life, a place, an interest, a recurring situation. There's no
nesting; everything sits at the same level and the drawer lists
articles alphabetically.

The wiki is a peer to Memory and Journal. Memories are atomic facts
the assistant references inline. Journal entries are dated reflections
about how a conversation went. Wiki articles are the longer-form
topical pages that cover what something IS - "the recipe project",
"Maya", "Lisbon trip planning". An article sits across many
conversations.

## Opening the Wiki

Click the **Wiki** tab in the left drawer. The sidebar shows the
alphabetical listing with a search bar at the top; the main panel
opens whichever article you click.

When the Wiki tab is open with no article selected, the panel shows
an empty-state hint plus an "add a new one" link that opens the
inline create form.

## What goes in an article

Articles are encyclopedic - they read like Wikipedia lead paragraphs,
not chat replies. Third person, present tense, neutral. They're meant
to summarize what you'd want to come back to later, not transcribe a
conversation.

Each article has just two fields:

- **Title** - the topic name. This is the alphabetical sort key in
the drawer and must be unique. The title cap is 200 characters.
- **Content** - the article body, in Markdown. Capped at 16,000
characters.

## Searching

The search bar above the listing filters the drawer in place. It uses
the same semantic-search pipeline the assistant uses for `wiki_search`
(see "How the assistant uses the wiki" below) - typing a phrase finds
articles by meaning, not just by literal substring. Substring matches
are merged in too, so an article you wrote ten seconds ago (before the
embedding worker has caught up) still surfaces.

Clearing the search returns the alphabetical listing.

## Adding an article

Click the empty-state link **add a new one**, or click **Wiki** in the
drawer with no article selected. The inline form takes a title and
content; **Save** persists immediately and surfaces the new article in
the panel.

Titles are unique per user. If you try to create an article with a
title that already exists you get a clear error and can either rename
the new draft or open the existing article and edit it.

## Editing

Open an article and click **Edit**. The view flips to a form with the
title and content fields. The form shows "Unsaved changes" the moment
you diverge from the stored row; **Save** persists, **Cancel** drops
the draft.

Saving an article nulls its embedding - the background embedding
worker will re-compute on its next poll (within ~30 seconds). Search
falls back to substring matches in the meantime.

## Deleting

Click **Delete** on the open article. A confirmation strip appears -
**Delete** is the destructive action, **Cancel** dismisses the prompt.

Deletes are hard - the article doesn't move to a trash bin. If you
delete by mistake the easy recovery is to ask the assistant to
reconstruct it from the relevant conversations and call `wiki_create`,
or to recreate it manually.

## Asking the agent to update an article

Each article has an **Ask agent to update** button that opens an
instructions textarea. Type what you want the agent to do ("add a
sentence noting that Maya prefers green tea", "fix the date in
paragraph two", "rewrite the second paragraph for tone but keep the
facts") and click **Ask agent**.

The agent runs on the spot and shows a preview. You then have three
choices:

- **Accept** - persist the agent's version. The article updates and
the listing reflects the change.
- **Try again** - throw away the preview and run the agent again.
Useful if the agent's interpretation didn't quite match what you
wanted.
- **Cancel** - close the preview without changing anything.

The agent is told to do exactly what you ask AND to preserve every
fact already in the article unless you explicitly say to remove or
replace it. So "add a sentence" adds without rewriting; "rewrite the
second paragraph" rewrites only that paragraph; "fix the date in
paragraph 2" patches that single value.

If your instructions don't actually require a change ("looks fine",
"no edits"), or are too ambiguous to act on without the agent
inventing facts, the agent emits a "no change applied" note with its
reasoning instead of a preview. You can Try Again with sharper
instructions or close the dialog.

## The autonomous background agent

A background agent reads conversations a day after they settle and
either updates an existing article or creates a new one. The
specifics:

- A conversation becomes eligible the day **after** its newest
message lands (in your timezone). A conversation that wraps Monday
evening is eligible Tuesday morning.
- If you continue the conversation, it becomes eligible again the day
after that. So Monday -> agent runs Tuesday -> you continue
Wednesday -> agent runs again Thursday on the new turns.
- The agent decides per topic whether to update, create, or do
nothing. The bar for creating an article is "would the user later
look this up?", not "did this come up at all?".
- When updating an existing article, the agent preserves every fact
unless the conversation directly contradicts it. The wiki accretes
rather than churns.

You can disable the autonomous agent in **Settings -> Wiki**. Manual
edits and the per-article "Ask agent to update" flow keep working
when it's off.

## How the assistant uses the wiki

Articles are **never** auto-injected into the chat. The assistant
reaches them only through `wiki_search`, an always-on tool registered
on every conversation. When you mention something topical or factual
about yourself - a project, a person, a place, a habit - the
assistant will call `wiki_search` to pull the relevant article so its
reply is grounded rather than guessing.

This is the deliberate split between the three knowledge surfaces:

- **Memory** - atomic facts, may be primed inline at the start of a
conversation.
- **Journal** - dated reflections; today's automatic entry is included
in the system prompt on the first turn.
- **Wiki** - encyclopedic articles, never auto-included. Always
retrieved on demand.

If you want the assistant to use a particular article, mention the
topic (or the title) directly - that's the cue for `wiki_search`.

## Settings controls

The **Settings -> Wiki** pane has one toggle: whether the autonomous
wiki agent runs in the background. The wiki uses the same day boundary
timezone you set on the Journal pane.
133 changes: 133 additions & 0 deletions src/components/WikiList.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<script lang="ts">
/*
* Sidebar listing for the user's wiki. Shown in the left drawer when
* the Wiki tab is active. Owns the search input - bound to
* `wikiStore.query` so a search keystroke filters the listing in
* place.
*
* Articles are displayed alphabetically by title (case-insensitive)
* regardless of recency - the wiki is meant to be browsed by topic,
* not by edit time. Click on a row sets `route.wiki_article_id` so
* the main panel renders that article's full body.
*
* Search is the same debounced semantic-search pipeline the assistant
* uses for `wiki_search` - see `searchWikiArticlesSemantic` in
* `$lib/wiki`. Drives `runWikiSearch` on the store.
*/
import { app } from '$lib/state.svelte';
import { route, navigate } from '$lib/routing.svelte';
import { wikiStore, runWikiSearch } from '$lib/wiki-store.svelte';

interface Props {
onSelect?: () => void;
}
const { onSelect }: Props = $props();

const SEARCH_DEBOUNCE_MS = 200;

let debounceTimer: ReturnType<typeof setTimeout> | null = null;

$effect(() => {
const _q = wikiStore.query;
void _q;
if (!app.supabase) return;
if (debounceTimer !== null) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
debounceTimer = null;
if (!app.supabase) return;
void runWikiSearch(app.supabase, app.venice);
}, SEARCH_DEBOUNCE_MS);
return () => {
if (debounceTimer !== null) {
clearTimeout(debounceTimer);
debounceTimer = null;
}
};
});

// Sort by title case-insensitively. Semantic-search results come back
// in similarity order; alphabetising at the view layer keeps the
// drawer reading as a wiki listing rather than a relevance ranking.
// Substring-only and empty-query results land here already alpha-
// sorted but a re-sort is a no-op on those, so this stays the only
// place that owns the order.
const sorted = $derived(
[...wikiStore.results].sort((a, b) =>
a.title.toLowerCase().localeCompare(b.title.toLowerCase()),
),
);

function pickArticle(id: string): void {
navigate({ wiki_article_id: id });
onSelect?.();
}
</script>

<div class="recipe-drawer-list">
<div class="wiki-list-controls">
<input
type="search"
class="sidebar-search-input"
placeholder="Search wiki"
aria-label="Search wiki"
bind:value={wikiStore.query}
autocomplete="off"
spellcheck="false"
/>
</div>
{#if wikiStore.loading && wikiStore.results.length === 0}
<p class="subtle" style="padding:0.75rem">Loading wiki…</p>
{:else if wikiStore.error}
<p class="error" style="padding:0.75rem">
Couldn't load wiki: {wikiStore.error}
</p>
{:else if sorted.length === 0}
<p class="subtle" style="padding:0.75rem">
{#if wikiStore.query.trim().length > 0}
No matches.
{:else}
No wiki articles yet. The background agent writes them as you
chat, or you can add your own.
{/if}
</p>
{:else}
{#each sorted as a (a.id)}
<div class="row thread-row" data-wiki-id-link={a.id}>
<button
class="thread grow"
class:active={route.wiki_article_id === a.id}
aria-current={route.wiki_article_id === a.id ? 'true' : undefined}
onclick={() => pickArticle(a.id)}
title={a.title}
>
<span class="wiki-list-title">{a.title}</span>
</button>
</div>
{/each}
{/if}
</div>

<style>
/* Mirrors the search-row styling used by the other drawer tabs. The
bottom border IS the divider between the search row and the
listing rows. */
.wiki-list-controls {
display: flex;
gap: 0.35rem;
align-items: center;
padding: 0.4rem 0.6rem;
margin-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
.wiki-list-controls .sidebar-search-input {
flex: 1;
min-width: 0;
}
.wiki-list-title {
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
</style>
Loading
Loading