Gå til hovedinnhold

S-02: Kapi Desktop

Summary​

Kapi Desktop is a Wails v3 application at apps/kapi-desktop/ (module github.com/neokapi/neokapi/kapi-desktop). It opens on the workspace, titled "Your context": what each project holds that the person has not seen, every project this machine account has run kapi in, whichever surface ran it (C-03), and the rules that hold in all of them. The record of what people and agents have recorded about their context (C-11) is a History view of its own. Projects open from there as tabs, several at a time, on a Map: the project's content files, the counts of its files, blocks, rules and findings, and the suggestions awaiting a person. Beside it, a Context hub opens on Decide, one question for each suggestion about how the project writes that waits for a person, beside the graph explorer, what an assistant working in the project reads, the voice profile, the terms, the content memory, the project's history and where its context is shared. Each tab also carries a project section with what the project stands at and a map of its coordinate points, a checks panel the Map's findings count opens, a review queue, governance editing on the recipe, a runner that brings the project up to date through the same venue the CLI uses, a tool reference, and an OS-keychain credential vault. It depends on the framework and host only. It links neither Cobra nor the cli module, and no package from the platform layer, which make audit-modules asserts by inspecting the transitive package list of ./backend/....

Context​

Everything the desktop does is available from the CLI. What it adds is the affordance: the whole resolved voice profile on one page instead of a YAML file to read, a map of where each collection is governed instead of a recipe to trace, a form driven by a tool's JSON Schema instead of a remembered flag, a progress view instead of a log tail, and a place to paste an API key that is not shell history.

Nothing about that needs a second implementation. host is cobra-free. Command threading is the host.Command interface, which *cobra.Command satisfies natively and which host.EnvCommand satisfies for embedded runs, so the desktop calls the same functions the CLI does, with a different thing on the other end of them.

The constraint that shapes the module boundary is the reverse direction: Wails brings a large native toolchain, and the CLI must never acquire it. So the desktop is its own Go module, and the boundary is asserted rather than trusted.

Decision​

Stack and module boundary​

  • Backend: Go, with Wails v3 generating TypeScript bindings from the exported service methods.
  • Frontend: React 19, Vite, Tailwind CSS 4, shadcn-based primitives.
  • License: Apache-2.0, matching the framework.

The module depends on the framework, host and Wails, and on no package from the platform layer. The licence boundary has no exception: a type both the desktop and the platform need lives below the line, in the framework or in host.

apps/kapi-desktop/
├── go.mod # module github.com/neokapi/neokapi/kapi-desktop
├── main.go # Wails entry point, application menu, embedded assets
├── backend/ # one flat Go package: the bound service
├── frontend/ # React + Vite; bindings/ is Wails-generated
└── build/ # per-platform build configuration

Four assertions run in make audit-modules: an isolated GOWORK=off build of ./backend/... (so the module resolves against its own go.mod, not the workspace overlay), a tidiness check that fails on any stale or boundary-crossing requirement, an import-level check that the transitive package list contains no Cobra and no cli package, and the same check for any platform package.

One bound service, many open projects​

backend.App is the single Wails service. The frontend calls its exported methods; there is no second RPC layer and no hand-written binding code. Within that one service the concerns are split across files: project lifecycle and status, the project's coordinate points, the context graph and the voice profile per point, governance edits on the recipe, flow CRUD, the runner, the catch-up loop and its venue, checks, the review queue, inspection, content memory, terms, media, outputs, the sample project, plugins, credentials and model detection, locales, settings, the workspace home and its watcher, the file watcher, and the in-app updater.

Projects open as tabs. Every project-scoped method takes a tabID, so the open set is real state in the backend. Each tab carries its own loaded recipe, project context, and resolved stores, and closing one releases them. A file watcher refreshes the tab when the recipe changes, including edits made through the CLI or git.

Every edit is a change set​

The desktop changes content through the change service and nothing else (E-09). Four bindings carry the contract: Read, Apply, Describe and History, each taking and returning its JSON as a string that the frontend types with @neokapi/contract-types. A string, because an operation is a union that marshals itself, and a model Wails generated from the Go struct would describe its fields instead. The backend builds the service for the tab's project with desktop as the origin, so the commit check governs every edit and the block history records it, and it sends every change set as the person at the keyboard.

A surface that changes content reads the block it shows through Read and sends what the person does with the revision it read:

  • The Review page edits a translation, or a source row's wording, in the inline-code editor: each code is a chip the person types beside, and the text goes back in the placeholder form the read showed, so a code the edit kept is the code the edition holds. A plural or select is edited a branch at a time, each branch in the same inline-code editor (inside PluralTargetEditor for a plural), so every code keeps its id whatever it displays, and each changed branch is a set_content addressed by the path the read lists. An AI proposal is accepted as the same set_content.
  • Approve and Reject send decide with the outcome establish or reject, Reject with the reviewer's note on the change set. A held-back draft, which reads as absent, is decided as absent. Approving the clean blocks of a view reads each one as it approves it and runs its checks again; a block whose text differs from what its row lists, or that trips a check, stops the batch with its row brought up to date.
  • A check finding whose rule names a replacement carries its fix, the replace_text kapi check reports, and the Checks panel sends it as it is. A finding whose words have an inline code among them carries none.
  • The document view puts a clicked block in focus and edits its source or a translation under the document (S-06). A translation is edited with its own plurals and codes, which the read lists beside the block.

When the content changed after the surface read it, the change service refuses the change with the edition as it stands. The surface shows that text under "Changed since you opened it" and asks before sending the change again over it, with what the editor holds at that moment. When a rule in force fails on the wording a person saves, the change service refuses the change as gate_failed with the findings. The editor shows them under "A rule in force fails on this wording" and offers "Save anyway", which sends the change set it refused again under gate: report: the edit lands with its findings, and the record of it lists them as overridden (E-09). Changing the wording puts the prompt away, and the next save is checked again, so an override only ever carries findings the person was shown. The desktop sends every change set as the person at the keyboard, and the change service refuses gate: report from an agent. A change set naming one translation is read and written with that translation's language, as kapi apply edits it: a PO catalog that holds its translation is edited in place, and the French of a catalog whose collection names a target lands in the file the target names (po/fr.po), never in the source catalog. A check's fix sent again finds its words by the text the finding quotes, and is offered only while those words are plain text in the text as it stands. The Review page lists the edition's recorded changes under its provenance, read from the block history through History, and its provenance names the person or the agent whose recorded change produced the text in force.

The app opens on the workspace​

The first screen is the workspace home, titled "Your context", in the overview shape of the page patterns guide (packages/ui/docs/page-patterns.md): a page header, a row of labelled counts, and two lists of rows that each lead to a project. Top to bottom it holds the counts, What's new, Projects, Holds in every project (drawn only while the workspace holds a rule) and Start here, which comes first instead on an empty workspace. The header names the workspace directory by its last segment, with the full path in the tooltip.

host.App.ContextStandings answers the counts, What's new and each project row's rules column from one fold of the log over every registered project. A ContextProjectStanding carries the project's rules in force, the suggestions advising, the conflicts, the concepts in its terms store, what is new since the person's marker (below), and the latest session's activity as a ContextActivity: its actor, what it recorded and corrected, what is new by digest theme, and Working, which holds while an agent session has added an operation within the last two minutes, the feed's quiet rule. Rules count what review decided, and terms count every concept in the store, imported ones included, so a project whose terms came from a file shows them beside zero rules. The terms figure is one COUNT(*) over the project's own context store in the workspace, which the host opens only when the workspace already holds it (workspace.Workspace.HasContext) and reads without binding the terms schema. A store that is missing or unreadable counts zero, and no two registered projects share a store, so the home's sum counts each concept once. The counts are drawn by StatRow: projects, rules, terms, suggestions, and the conflicts as "needs you", in the warning ink while there are any. No sentence on the page is generated from a number. What's new lists the projects with something new, a suggestion advising, a conflict or a working session, newest session first. A row is the latest session's actor as an ActorMark, the project and how long ago, one chip per digest theme the session noticed ("3 names", "1 word") and a warning chip for the conflicts, then one button, Decide while there is a conflict and Review otherwise, which opens the project's tab on Decide. A working session adds a chip with a live dot.

Each project row is one line of aligned columns: the voice profile governing the project's default point, the rules in force, an icon and one word for where its context is shared (ProjectSharing, under Sharing and the ladder of reach) and the last activity. Open opens the first checkout still present; the row's menu holds the other checkouts, Copy path and Remove, and the first checkout's path is the tooltip on the name. Holds in every project lists the rules applied to the workspace (WorkspaceRules), each with the project it was decided in and how it came into force. Those rules live in the workspace's own rule table, which every project on this machine reads.

Start here is three steps for a first run: open a project or try the sample, connect an assistant, come back to What's new. Dismissing it is a setting in the desktop's own settings (StartHereDismissed, through DismissStartHere and RestoreStartHere), the way the sample cards are dismissed, so it stays dismissed across launches until Settings restores it.

The feed of every project's operations is History, a view reached from a link beside the title (the feed).

The Projects section reads the workspace's project registry (C-03): every project this machine account has run kapi in, whichever surface ran it. A project registers the first time kapi opens it, so the desktop lists a repository set up from a terminal without being told about it. The app registers too, from OpenProject and NewProject, through workspace.Register rather than through the project store, because opening a project to look at it is no reason to create .kapi/work/store.db inside somebody's checkout.

A registration carries the project's identity, its display name, the checkout paths it has been seen at on this machine and its last-active time. Checkouts are a list rather than a field, so two worktrees or two clones of one repository are one row with two places to open it from. A checkout whose recipe has gone is marked missing and kept: the project and its context are untouched by a folder disappearing, and dropping the row would erase the only record of where it was. A project with no checkout here opens as a context-only tab: it carries no recipe and no files, its terms and content memory are bound straight to the workspace's context store as borrowed handles, and the Context hub drops the sections that read a checkout.

Workspace.Forget deletes the project registration, checkout records and context store after confirmation. The confirmation identifies the database to delete and the checkout folders to preserve. Closing a tab, deleting a checkout or resetting a sample leaves the registration and context intact.

The desktop detects changes made by the CLI and MCP server through the shared workspace operation log. A goroutine started in ServiceStartup polls workspace.Head once per second and emits workspace:changed when the revision changes, triggering a frontend refetch. Reopening an unchanged registration leaves the revision unchanged. This requires one indexed query per second and avoids interpreting filesystem events from SQLite database and WAL files.

The Map​

Every project tab opens on its Map, and a persisted view opens where it was. The project rail lists, in order, Map, Context, Project, Review (only while the project declares target languages), Toolbox and Project Settings; the checks panel has no entry of its own. A project waiting to be set up, an empty folder or one matching a preset, opens on the Map too, and the Map's empty files list leads to the project section, where the setup and the preset are offered. The Map is an overview in the page patterns guide's sense, and it renders from one bound call, MapSummary, which answers from what the app already holds and reads no file the store sync has not read:

RegionWhat it showsRead from
Headerthe project's name; when the store sync last found the block cache current with the files on disk ("Read 2 minutes ago"), naming how many files the cache holds only when that differs from the files count; Run on everything; Decide with the count of conflicts and suggestions awaiting a personthe time SyncProjectStore records on the tab, the block cache
Countsfiles (the recipe's resolved content files), blocks (the cache's translatable blocks for them, as GetProjectStatus counts them), rules (the concepts in the bound terms store, the word and pattern rules of the voice profile at the project's own point, and the notes a person kept) and findings (the last checks run over every file), the only one in the warning ink and only above zero. Blocks open Ask, rules open Explore, and findings open the checks panel. kapi suggests beside them shows only when conflicts make its number differ from Decide'sthe block cache, the terms and voice stores, the operation log, the tab's last whole-project RunChecks
What your files doone row for each conflict and each suggestion the digest holds: its kind as a KindIcon, the rule, the quotation, and the distinct blocks and files its evidence names. Decide opens the Decide section selecting that row (conflict:<id> or suggested:<id>)host.App.ContextDigest, and the operation log for the evidence
What your files do, empty"No suggestions yet." and Connect an assistant, which opens the Assistants section
Filesone row per content file: format, name (the folder on the meta line), blocks, findings when above zero, and Open, which previews the file. Most findings first, then by path. An empty list offers Add files, or Set up the project while the project waits to be set upthe resolved content, the block cache, the last checks run

The checks run behind the findings count is RunChecks over every file in the recipe's target languages, the same run the checks panel makes. A run that narrows the files leaves the Map's counts as they were. The tab keeps the last run over every file and every target language in memory; a narrower run, from the checks panel under the menu-bar filter, does not replace it. The Map runs one itself the first time it opens a tab that has none in this session, and Run on everything runs it again. The findings count opens the checks panel on that run (LastProjectChecks), so the count and the panel agree until the person runs the panel's own checks.

The project home and the point map​

The Project section shows the project's standing, in the two-axis shape kapi status prints: content on one row, governance on the other, and the venue and stream on the identity line. Below it, the point map lists every coordinate point the recipe declares (C-02), the collections at each and the voice profile governing there. A row opens the Context hub standing at that point. A single-point project gets one row rather than no map.

Between the standing and the point map, the project lists the conflicts kapi status reports, when there are any, block by block with both wordings (GetKeptConflicts). One kind is an edit another machine made to a draft the workspace keeps, which a pull merged and which could not land: the workspace holds one wording and that edit another. The other is wording a person kept in the workspace whose translation's file has appeared since without it: the file holds one wording and the workspace another (C-03). The person keeps either side or writes a new wording, and each choice that writes is a set_content sent through Apply with the revision the conflict shows as held, so a block that moved meanwhile is refused stale and shown again. Keeping the wording the workspace holds is recorded as the person's decision, and a translation whose file exists has the workspace's copy released once the file holds the chosen wording (ReleaseKeptWording).

A third kind is a version of a document a KPZ in the project carries that was written from an older version than the document holds now, such as a .kpz replaced on disk while its cache held unpacked edits (E-09). It lists no blocks at first. The person rebases it (RebaseKeptDocument), which carries its changes over onto the document through the change service and lists each block both versions changed, decided as above, or discards it (DiscardKeptDocument), which keeps the document as it stands. A rebase the change service refuses, for a stale block or a failing gate, shows the reason and settles nothing.

The workspace home lists the same kind for every .kpz on this machine whose working cache holds edits, inside a project or outside every project (GetWorkspaceDocumentConflicts). The caches are the user's own, one per .kpz and keyed by its absolute path, which their metadata records, so the list needs no project open, and each document is named by that path. Rebasing, discarding and deciding a block (RebaseWorkspaceDocument, DiscardWorkspaceDocument, ApplyWorkspaceDocument) go through the change service of the project the .kpz sits in, or of its directory when it sits in none.

The backend resolves the declared cross product, the project's own point and then each profile's channels, and lists a collection where it is actually governed: a collection whose profile's window has closed appears at the point that governs it, not where it was written.

The Context hub​

The stores are sections of one hub, beside the graph, in this order:

Section (id)What it showsAnswered by
Decide (rules)one question per suggestion waiting for a person, the questions kapi rules asks (below); on a tab with no checkout, the digest of what was recorded, without the decisionsRulesDecideView and DecideRule, over host.App.DecideRules and host.App.DecideRule; ProjectContextDigest for a tab with no checkout
Explore (atlas)the project's context on its axes and over time, through @neokapi/context-atlas (below)ProjectContextAtlas, over host.App.ContextAtlas
Ask (explorer)the context graph by file and by word, through @neokapi/context-explorer; the governs pane renders the same guide the retrieval surface serves (C-06)ContextAt, ContextGoverns, ContextLives, ContextRelates, ContextSearch
Assistants (agent)each assistant and its connection, with Connect, and the newest sessions recorded in the project (below). Show what it reads opens the rules files and the brief for one file. The brief is the resolved context for one file as an agent receives it: AgentContextAt calls the desktop's ContextAt and renders the answer's own FormatText, so the body on screen is the context:// resource body rather than a second rendering of itAssistantsView, ConnectAssistant, ConnectAgentHost (Claude Desktop), RulesFiles, WriteRulesFiles, AgentContextAt
Voicethe whole resolved profile per point: tone, style patterns with severities and rates, term rules, examples, and the locale, channel and persona overrides as authored (C-07); the profile is edited hereProjectVoice, SaveVoiceProfile
Termsthe terms store: search, facets, provenance, concept relationsSearchTerms
Content Memory (memory)the content memory: search, facets, activity, provenance, the languages gateSearchMemoryEntries
Sessions (history)the project's feed (below), with a reset to before each session. It is not on the section bar: a session on the Assistants page, or All sessions there, opens it, and the bar marks Assistants while it is openContextFeed with the project key, ContextResetScope, ResetContext
Share (share)how far the project's context reaches, as three rungs (below)ProjectSharing, SetProjectSharing, SyncProjectContext

Each id is routable, so a persisted view and a link from the home or the Map land on its section. Each section names the command that does the same on the command line, as a chip at the right of the section bar: kapi context review, kapi context atlas, kapi context log, kapi context sync, kapi connect.

The voice section resolves every point the recipe declares twice: as declared, and at the governance instant. A binding whose window excluded the instant is drawn as a skipped rung with its boundary date and what governs in its place.

The terms and memory pages browse and audit. Moving store contents in and out as files is the CLI's job (kapi memory, kapi terms), so the desktop offers no import or export control, and scripts/check-desktop-interchange.sh keeps one from returning. The view ids the store tabs had before the hub stay routable so a persisted view still lands, and the ad-hoc rail keeps both stores, having no project to file them under.

Exploring the context​

Explore loads the project once per visit through ProjectContextAtlas, which returns the kapi context atlas --json document (the atlas contract): the axes, every record with its full status history, and the dated events. For a tab with a checkout, the document includes the content cells. For a context-only tab, it is built from the log and the stores only. The frontend draws every view from that one document, so moving the window, the columns or the snapshot makes no backend call.

@neokapi/context-atlas contains the views and a React-free model beside them. The model implements:

  • the query: OR within an axis, AND across axes, a window, a snapshot that follows the window's end, and the recorded or the effective clock;
  • the state of each record at an instant;
  • the derived axes: state at the window's start and at the snapshot, lifetime, and age;
  • the adjacent-pair layout of the flow and the A/B comparison.

A test, a host or an agent can therefore evaluate an exported investigation against the same data without rendering it. The page is an overview with a supporting pane for the selected record (packages/ui/docs/page-patterns.md). Selecting an evidence path opens Ask at that file.

The map draws a geo axis over the places in the atlas (C-12). Its default mode is Shapes, which draws real outlines. It starts at the narrowest region, country or place made of countries that contains every declared place and every place content sits at, so a note kept for a distant market does not widen the first view. With the geo axis filtered, or in a project that names no place below the world, it starts at the narrowest such place that contains the selected records. It drills from the world to a country, to its subdivisions and to the project's own places, with a breadcrumb built from the place's path. A place is filled when records sit within it. It is hatched when a record at a wider place, or at a place made of places that covers it, holds there. Records at world are shown as one Held everywhere count instead of a hatch over the globe. A declared place is drawn from its own geometry, else as a circle at its centroid, else listed beside the map. The map flags a declared place whose centroid falls outside the outline of the place it sits within. Places made of places are chips beside the map, and hovering over one outlines its members. The side list shows, for each place, the records Here and the records Held through a wider place. Every place in the list is a button with an accessible name and count. When the view has up to 30 places, the list shows all of them, so a county can be picked by name. In a larger view, such as the world's countries, the list shows only the places that have records within them, that the map cannot draw, or that are flagged. Selecting a place filters to the records at every place that overlaps it, by the same overlap rule as the engine. Grid draws one equal tile per country instead, for comparing counts independently of land area. The outlines are Natural Earth data that ship in @neokapi/context-atlas, loaded per country as the map drills in.

The feed of recorded context operations​

The app is meant to sit open beside an agent that is working. The agent runs in another process, through the CLI or its MCP server, and records what it learns as context operations in this machine account's workspace log (C-11). The desktop reads that log and shows it in four places: as each project's questions in its Decide section, as the sessions on its Assistants page and in its Sessions section, and as the workspace-wide History view the home links to, which is the same feed read with no project key.

The feed groups by session. An agent session is the id its operations carry, so one run is one card however long it lasts. A person at a terminal and a tool record no session, so their operations group by actor and local day, which is the span a person recognises as their own work. Each card carries the counts a finished session reads out in one line (suggestions, corrections, kept and dropped), and says "still working" until nothing has been added for two minutes.

Each operation shows its kind, its actor with the machine an agent ran on, the rule or wording it is about, its status (suggested, established, contested, withdrawn, dropped or reset, with a contested entry reading "Contested by #n"), and its evidence: the file, the block and the quotation. Evidence is on the card rather than behind a disclosure for anything awaiting a decision, so reviewers can assess the supporting text.

Deciding goes through the host API and nothing else. Keep, keep with an edit, drop a suggestion or a rule in force, widen, and reset the context to before a session are the backend's KeepContextSuggestion, DropContextSuggestion, WidenContextRule and ResetContext, which call host.App.KeepContextOperation and the host's other context operations. A reset is confirmed first: ContextResetScope lists what it would set aside, and the rules among them, without recording anything. The host owns the policy about who may do what; the desktop re-implements none of it. The acting actor is contextop.Actor{Kind: ActorPerson} with no name, because the app holds no account and the person at the keyboard is the one acting. Reading is contextop.Ledger over the workspace, because the host's reads resolve a project from a recipe path and the home screen spans projects, some of which have no checkout on this machine. A project with no readable checkout is read here and not decided on, and the card says so.

Widening requires confirmation. The preview lists affected projects for workspace scope, or newly covered recipe points when removing an axis restriction. It explicitly states that affected content has not been counted; the backend does not provide that count.

The keys are the review session's, so the two decision surfaces feel the same: j/k and the arrows move over the suggestions, a keeps, r drops, e opens the edit, and a field with focus keeps its own keys. A contested suggestion can be dropped at once and kept only after a person drops the other side. The workspace watcher is what keeps the feed current, so a suggestion recorded elsewhere appears within a second. The previous answer stays mounted through the refetch, which is what keeps the scroll position and a half-typed edit through an agent recording in the middle of it.

The home's What's new rows carry what each project's digest holds that the person has not seen (host.App.ContextStandings, one fold of the log for every registered project). A project with nothing new has no row: the home carries no count of work waiting.

Decide​

A project's Context hub opens on Decide, titled with the project's name, with the number of suggestions to decide and the number of rules kept. The Map opens it too, selecting the row a person chose there (conflict:<id> or suggested:<id>, matched against a question's own id or one of its sides'). Deciding there is discovery, never a gate: a suggestion advises agents and checks from the moment it is recorded, and one nobody answers keeps advising.

The page is a list beside a detail pane. The list holds one row per suggestion waiting for a person: its kind as a KindIcon, the wording it is about, and how much it covers (3 forms, 1 word, 1 of 1 blocks for a disagreement). The pane asks one question about the selected row. RulesDecideView answers with the questions kapi rules asks (host.App.DecideRules), so the terminal and the desktop ask the same thing in the same order:

SuggestionThe question and its options
A name"Which one do your files use?": each form with the blocks and files of the project's source that write it, the most used first, and Other… for a form the person types
A word, a note or a content-memory pair"Keep this as a rule?", with no options
A disagreement"Which one holds?" (or "Which one do your files use?" for two names): one option per side, each with who suggested it, its blocks and files, and what keeping it does

Before the actions, a line states what keeping the picked option does: the blocks a check then reports, and where the rule applies ("If you keep "KapiMart": no block uses the other 2 forms. Applies to Website."). A form typed under Other… is counted through RulesDecideConsequence, over the blocks the last read counted. Below the actions, Seen in quotes the sentence the suggestion was seen in, names the file (which opens Ask there), and shows who filed it.

Keep (k), Not a rule (n) and Later (l) answer; the option numbers pick an option. DecideRule(tabID, id, answer, form) records the answer through host.App.DecideRule, which makes the calls kapi rules makes for the same keys: a keep is DecideContextReview with the suggestion kept in the picked form, or with the picked side chosen; Not a rule drops the suggestion, or every side of a disagreement; Later records nothing and leaves the suggestion advising, and the page leaves it out until it is opened again. The first keep writes the recipe of a folder opened without one. After each answer the next question is selected, and with none left the page says "Nothing to decide." with the number of rules kept and a link to the Map.

Counting reads every block of the project's source once per view. The tab holds the last view, and the next read counts over its blocks again while no source file and not the recipe has changed (by path, size and modification time), so an answer does not cost a read of every file. The page reads again on workspace:changed, project-files-changed and project:extracted.

"Since you last looked" is a marker per project in this machine account's kapi config (context-digest.json under the config root), never in the log. The home's What's new opens a project on Decide while a suggestion or a disagreement waits there, and on Sessions otherwise. Decide moves the marker once it shows a question, and Sessions moves it as it opens, since it lists every operation.

A tab with no checkout on this machine has no source to count uses in. Its Decide section shows the digest instead (host.App.ContextDigest, the call kapi context review lists): the conflicts, the suggestions by theme, the rules in force and the drift, without the decisions. Rules already in force are dropped and widened from Sessions.

Sharing and the ladder of reach​

Share shows how far a project's context reaches, as three rungs: on this machine, shared through a backend, and a managed venue. The current rung is marked, and the project rows on the home carry the same answer as one word.

The backend resolves it through host. ResolveContextBackend returns the ContextBackendInfo the recipe declares: the kind, its location, and whether it came from the recipe or the default. ContextSyncStatus returns how far apart this machine and the shared copy were at the last sync, without contacting the backend. ProjectSharing folds the two with the workspace directory on this machine and the venue the recipe declares into one answer. A project with no readable checkout here is local, from the default.

The first rung is the workspace itself (C-03): every clone and worktree of the project on this machine reads the same context store. The second offers the three backends C-03 defines: git, a ref of the project's own repository outside every branch; file, a directory; and s3, a bucket with a prefix, a region and an endpoint. Set up sharing is SetProjectSharing, which writes the context: block into the tab's recipe through the same project update path a Project Settings edit takes, so undo, redo and the file watcher see it, and which refuses an unknown kind in the words the CLI uses. Credentials stay out of the recipe: each backend reads its own access on the machine. Sync now is one host.App.SyncProjectContext, the call kapi context sync makes: pull and merge, push, and rewrite the rules files. When the backend cannot be reached, the host's error is shown unchanged and nothing changes on this machine. A sync that merged anything emits workspace:changed, so the home refetches.

The third rung is the convergence venue the recipe declares, read the way the runner reads it. A venue and a backend are separate keys of the recipe: the backend is where the context's operations are shared, and the venue is where the project converges and a team reviews. The rung shows whether a venue is declared and where it points, and offers no sync of its own.

Connecting an assistant​

The Assistants section has one row for each assistant kapi connect knows (Claude Code, Cursor, VS Code, Codex, Agent skills) and one for Claude Desktop. AssistantsView reads the rows through host.App.ConnectHosts with HostsOnly: a dry run over each assistant's own files that plans no rules files, resolves no context and opens no store, so the page can be read on every workspace change. An assistant is connected when that run would leave each of its files as it is and the project's rules files hold kapi's section (host.HasRulesFiles); with the files in place and the rules files gone, the row reads "rules files missing" and offers Connect. The Claude Code row reports the plugin's check as on or pending, and while pending names the two commands host.ClaudePluginCommands returns. ConnectAssistant runs ConnectHosts for one assistant, as kapi connect <assistant> does, after the desktop writes the recipe of a folder opened without one, so it writes the files the CLI writes: the rules files, the assistant's MCP entry and its skill.

Claude Desktop reads one user-level file, claude_desktop_config.json in its application-support directory. The host layer writes only inside a project, so that entry is the desktop backend's to write, through ConnectAgentHost. After the person confirms, it adds a kapi server entry that names the absolute path of the kapi binary and the recipe, and keeps every other key in the file.

Below the assistants, the page lists the newest sessions of the project's feed (below) with their counts. The assistants and the sessions are read apart, so a failure in one leaves the other on the page. A session opens the Sessions section on it.

Show what it reads, in the page header, opens two tabs. The first shows the rules files (C-06) as kapi would write them. RulesFiles reads through a read-only planner in host, which resolves the sections WriteRulesFiles writes and writes nothing, so opening the tab changes no file. Write them now is host.App.WriteRulesFiles. The second is the brief for one file, described in the hub's table. A context-only tab has no checkout to write into, so it shows no assistants and keeps the sessions.

Governance editing​

Project Settings carries a Governance section: the voice profile bound as defaults.voice, the declared axes of the project's default point, the default flow, and the paths a content scan skips. The collection editor sets the channel a collection names and the axes it declares there, and a collection row shows the point it resolves to.

The refusal for the structural axes has one home. product and channel are derived from a collection's channel, so the setter calls project.DeclarableAxis and the desktop serves the same error the CLI does; the editor cannot offer an axis kapi apply would reject, or word the refusal differently (C-02).

The block cache follows the files​

The block cache in .kapi/work/ is how the app reads a project, and building it is local work that calls no provider. The app therefore keeps it current without a person's step. SyncProjectStore compares the resolved source files with their extract-time stamps (DetectStoreDrift) and runs the shared ExtractToProjectStore path only when the cache is missing, was written by another kapi version, or differs from the files. The frontend calls it when a project tab becomes active and after the file watcher reports a change. It returns without extracting while a run holds the project, because kapi up extracts before each pass.

The hero, its verb and Plan… appear only while the saved recipe declares a target language, the answer of declaresTargetLanguages in frontend/src/lib/languages.ts that the rail's Review entry also reads. On a project without one the runner route opens no Bring up to date view either.

The project home reports the cache only by its effect: while it catches up, the hero reads "Reading the project's files…", and a failed read is shown as an error with its cause. Below the hero's verb, three counts separate the run's work from a person's: blocks to translate, translations waiting for review (which open Review), and collection languages held back, each listed with what withholds it (convergence.LocaleCoverage: a ship-gate shortfall, stale or rejected wording, a failing check, an unchecked term). A manual read of every file stays under the collections' Advanced actions.

Runs go through the up venue​

"Bring up to date" calls host.App.RunUpDispatch, which resolves where the run executes through host.App.ResolveUpVenue: locally, or at the convergence venue the recipe binds. The CLI and the MCP up tool take the same route, so a project bound to a venue converges there from every face and the server keeps the state a team reviews from.

Live progress survives the dispatch. The venue route reads the plumbing's NDJSON line by line as it arrives, folding framing records into the result and handing the run's own events to the run view; a buffered document and a streamed one are read by the same fold. The shared engine discovers plugins once, and a run borrows that engine, so the venue a run resolves is the one the tab's other surfaces see.

FrontendReactbackend.AppWails serviceExecutorhost + frameworkRunFlow(tabID, flow, inputs, locales)executeflow spec + project contexttrace recordsstep, block, log, errorflow:eventemitted + buffered for reconnectCancelRun()context cancel

A run is one call in and a stream of events out. The backend buffers the stream so a reloaded window replays the run rather than losing it.

A desktop window can reload mid-run, and a run that only existed as a live event stream would vanish. The backend keeps the events, collapsing consecutive metric snapshots so a long run does not grow the buffer at the sampling rate. The runner asks for a target language only when the flow produces one.

The toolbox is a reference; flows run through the runner​

The Toolbox page describes each registry tool: what it does, its schema, what it consumes and produces, its reference page, and the command that runs it (kapi exec <tool>, with kapi tools schema <tool> for its options). Tools execute through the CLI and through flows; the desktop runs no tool on its own, because a third path would have to re-solve project isolation and AI consent for little.

A project's flows page lists what kapi flows lists for the project: the recipe's inline flows, then the files in its flows_dir: that no inline flow shadows, a file that will not load shown with its problem. RunFlow, GetFlow and the preview resolve a name the way kapi run does (host.ResolveProjectFlow, E-04). The editor saves inline flows into the recipe and opens a flow file read-only, since the file is where that flow is edited.

The flow editor's Run action goes through the same RunFlow path the collections table and the project home use. It appears in project mode, where there is a project for a run to act on, and stays absent in ad-hoc mode.

The flow editor's graph edits composition only. Its two ends are endpoint pickers (file, store, interchange, or none), not draggable reader and writer nodes, because the binding is a property of the run rather than of the flow (E-04).

The sample project​

A first-run sample project is scaffolded from backend/sample/: a governed recipe, and context read into the project's stores from a .terms.json bundle, a voice profile and a .memory.json bundle, the native serializations (M-06), loaded with the same reader the rest of the tree uses. Seeding bulk-loads the entries and then rebuilds the search and fuzzy side-tables, so the sample's memory answers search as well as exact lookup.

After the context import, the sample records two assistant sessions through the host's recording calls (RecordContextObservation and RecordContextCorrection), as claude-code and then codex, each under a stable session id. Every piece of evidence quotes text in the sample's files, because the host refuses evidence it cannot find. The first session notes a name, a word to use, a spelling, a correction a person made and one observation about how the store copy addresses the reader. The second takes the other side of the correction and notes one more name, so Decide opens on a disagreement, and a first-time user practises keeping, marking not a rule, leaving for later and resetting on the real surfaces with no model call.

Before the agent sessions, the seed records six notes as a teammate and keeps them. The notes are established from the start, and Decide still opens on the assistants' suggestions. The notes give Explore data on each declared axis. The sample's recipe declares places (the Nordics, Oslo, Grünerløkka and Greater Oslo) and types market as a geo axis over them, age as a range and lifecycle as an ordered axis (C-02). An Oslo youth campaign sits at market: oslo, age: 13-17, with its pick-up page at Grünerløkka. Five of the notes have a point: Japan, Germany, the Nordics, ages 13 to 17, and the campaign's city and age together. Three have windows set relative to the day the sample is created. Whenever the sample is opened, one of those windows is in force, one has ended and one has not yet started.

Seeding is idempotent: it runs only when the project's log holds no agent session, read from the ledger, so reopening or resetting the sample records nothing twice.

A scaffolded sample records the revision it was written at in .kapi/sample.json, and sample.Revision is the revision this build ships. The home of an older copy that still loads offers the upgrade. An older copy whose recipe this build rejects is refreshed when it is opened, from Try the sample or from the workspace home. The old directory is kept beside it as <name> (backup rN), and the current sample is scaffolded in its place. For a sample at the current revision, and for any project that is not a sample, the app reports the load error and leaves the directory unchanged.

Three faces, one record​

kapi answers the same questions from the CLI, over MCP, and from the desktop. A face parity suite holds the three to one recorded answer: host/facetest writes one fixture from one description and embeds one set of answers, and each face's own suite builds the fixture and compares its reply, the desktop's through its backend methods. The shapes are projections rather than the faces' own structs. A known gap between two faces is pinned by a test that asserts it, so closing the gap fails the test that describes it.

The frontend consumes the workspace packages​

The desktop is one consumer of the monorepo's shared frontend packages rather than the owner of any of them: the shadcn primitives and the preview kit from @neokapi/ui-primitives (S-06), the xyflow-based @neokapi/flow-editor, the graph views in @neokapi/context-explorer, the atlas views in @neokapi/context-atlas, the lab explorers in @neokapi/kapi-lab, plus the contract types, reference data, status views, concept views, and grid editor packages. All resolve through the single root pnpm workspace.

Its palette comes from the same place. The logo-derived palette is one file, packages/ui/src/styles/kapi-colors.css, which the desktop imports rather than declaring the values itself, and which in turn imports the semantic tokens that say what a colour means: the judgement colours and one hue per coordinate axis. The Storybook renders through the desktop's stylesheet, so a story and the app read the same values, and the documentation site's Infima variables and the diagram kit's defaults are computed from that one file. Ivory surfaces, cocoa accents and turquoise highlights follow the existing logo; dark mode uses warm charcoal and ochre with dark primary-button text. See Brand tokens and the documentation palette.

It also consumes @neokapi/i18n-react (S-05) for its own interface languages. The desktop's UI strings go through the same extraction and runtime the framework offers to any React application, and the target-language catalogs are build artefacts, regenerated rather than authored.

Configuration lives in two roots​

Provider configuration and installed plugins live under the kapi config home, shared with the CLI: providers.json there, API keys in the OS keychain under the kapi service name, and plugins under plugins/ (S-01). The CLI and the desktop both take an alternative plugin discovery root from KAPI_PLUGINS_DIR, and both honour KAPI_PLUGINS_DIR_ONLY to skip the user/system roots entirely, the same isolation contract dev, CI, and the harness recorder already rely on.

The desktop's own preferences (theme, interface language, hidden and custom locales, telemetry consent, the session's open tabs) live in a separate root, <UserConfigDir>/kapi-desktop, overridable with KAPI_DESKTOP_CONFIG_DIR. The split is deliberate: a preference about a window is not a setting the CLI should read, and resetting one should not disturb the other.

It reuses framework primitives rather than re-deriving them​

Tool listings come from the same registry the CLI reads, and each tool's schema form is generated from its JSON Schema (E-03), so a new tool appears in the desktop with no desktop change beyond registration. Formats, plugins, providers, block stores, the context graph, and the executor are likewise the framework's, consumed rather than mirrored. A project declaring a different block store opens without the desktop knowing which one: resolution goes through the BlockStore interface in the project machinery.

Differences from the CLI are presentational: dynamic forms, event streaming, live progress, tabbed state. There is no desktop fork of framework behaviour.

Distribution​

The release workflow builds the desktop across a platform matrix covering macOS, Windows, and Linux on both common architectures where the toolchain allows it. macOS ships as a Homebrew cask from the tap, Windows and Linux as archives attached to the GitHub release. A signed appcast feeds the in-app updater, so an installed desktop can update itself without going back through the package manager.

Consequences​

  • The desktop reaches every framework capability through host, so it can drift from the CLI only on presentation, and the face parity record catches drift on the answers themselves.
  • Wails and the native toolchain stay out of the CLI module, which keeps the CLI cross-compilable and small.
  • The cobra-free and platform-free assertions are mechanical: a reintroduced cli or platform import fails make audit-modules, not a review.
  • A project bound to a convergence venue converges there from the desktop as from the CLI, so the team's state has one home.
  • The desktop writes the recipe's context: block, the agent wiring and the rules files through the host calls the CLI makes, so a project shared or connected from the desktop is the project kapi context sync and kapi init see.
  • Tabs support several open projects. Each project-scoped method requires a tabID to route operations to the correct project.
  • kapi.yaml recipes stay shareable workflow documents: open, edit, save, commit. No hidden state travels with the recipe, and a governance edit made in the desktop is a recipe edit git diff shows.
  • Any new tool, format, or provider registered in the framework appears in the desktop with no backend change.
  • The app lists projects registered by any local kapi surface and checks for updates with one indexed query per second.
  • Every desktop edit passes the commit check and lands in the project's block history with the desktop as its origin, and a change made elsewhere since the desktop read the content is shown to the person before anything overwrites it.