Think¶
Your thinking space — where anything starts.
Every object ZettelFlow has presupposes the thinking already happened.
A note wants a title and acquires a lifecycle state. A typed relation is a judgement you already made. A claim has a source. An inquiry starts from a concern you can already state. Even Cultivate asks what shall we do with this idea — which assumes you have one.
There was nothing, anywhere in the product, for "I don't know what I'm thinking yet."
And that is the state a thought is in when it matters most: absurd, contradictory, intuitive, speculative, half-wrong on purpose. The old answer was write a note — and a note immediately asks for a title, becomes an orphan, shows up in Health and adds to your debt. The system asked you to be finished before you had started.
The Lab is the first arrow.
In the family (#707, epic #701). Think is the mode the other two learned from: writing comes first, it speaks in a human voice, and it is one calm stream. Home and Cultivate now share its shape — the same mode bar, the same composer frame (Home writes into Think from its own page), serif for what you think, and the same quiet hints. Think itself gained the family's composer frame and a marked passage reads as a quote with an accent edge. Throw away (Shift+D) sends a thought to the Obsidian trash with an inline Put it back; Set aside never deletes — it only takes a thought out of your way.
A thought asks for nothing¶
| A note | A thought |
|---|---|
| a title | — |
| a lifecycle state | — |
| structure | — |
| to be right | — |
| becomes an orphan | never |
| counts toward debt | never |
| appears in Health | never |
What it carries: when it was written, what it says, and optionally that it forked from another thought or challenges one. Three words is a thought. A contradiction is a thought. So is something you wrote deliberately wrong to see what it looked like.
Not knowledge, by construction¶
This is the part worth understanding, because it is the whole safety of the idea.
The Lab folder goes into scopeExcludedPaths — the same array that already carries ZettelFlow's own flow, hook and library folders (#311). That one place decides what is not knowledge, and everything downstream reads from it: the KnowledgeIndex, Health, debt, Discovery, orphans, resurfacing, Cultivate's queue, every projection.
No second mechanism. No flag a future change could forget to check. A thought is invisible to the system for exactly the same reason a flow canvas is.
The guardrail (test/architecture/knowledge/thoughtScope.test.ts) does not check the setting. It builds a real model over a vault whose Lab is full and demands zero — because the useSettingsHost load-order trap (#374) once made excludedPaths silently return [], turning every exclusion into a no-op while the settings object looked perfectly correct.
Your files, in your vault¶
Thoughts are markdown files in a folder you choose. Not data.json: data you cannot open with your own tools is not yours, and the promise of this layer is that what you write here remains yours even though the system ignores it. Open them, search them, sync them, grep them.
The text is the body, so what Obsidian shows is what you typed:
---
zfThought:
id: a1b2c3d4
at: 1758153600000
links: [e5f6g7h8]
challenges: e5f6g7h8
---
maybe the problem isn't that AI thinks worse than us
A highlight made in the Reader (#671) is a thought too: about the note it was read in, with the passage it quotes kept as an anchor, and your margin note as its body. The Lab shows the passage above the text, with Open in the Reader to go back to it:
---
zfThought:
id: f9e8d7c6
at: 1759660800000
about: Notes/Event sourcing.md
quoteExact: "stores changes, not state"
quotePrefix: "Event sourcing "
quoteSuffix: ". Replay rebuilds it."
quoteHeading: "Events"
---
this is the whole point: the log is the truth
The passage is found again by its words on every read, never by a marker in the note, so the note is never written. Passage fields are JSON strings, so a colon or a quote in the text is safe.
A highlight comes back for a second look a few days later (#678): a line above the composer, A few things you marked are back for a second look, shown only on a day one is due. Its review is a few more review* lines in the same frontmatter. Changed my mind writes a new thought under the highlight, marked as such, and crystallizing a highlight (here or from a card) quotes the passage and ends the note with source:: [[note#heading]], so the note counts as sourced (#679). See A few things you marked.
A file in that folder with no zfThought at all is still a thought — just text. Someone will write a note there by hand, and that is allowed. A refuge that rejects what you put in it is not one.
Writes go through FileService like every other write (#456), so a thought lands in the write record and can be taken back.
The folder¶
Settings → Thinking → Thought Lab folder. Default _ZettelFlow/lab. Point it at an empty folder: anything already in there becomes invisible to the knowledge model.
The space¶
Home → Lab, or the Think command. A mode of the Home surface, not a fourth leaf type: ModeHostView already owns the switching, the deep links and the lifecycle, and a new view would be exactly the addition subtraction exists to refuse.
The command's whole promise is a blinking cursor. No modal, no folder prompt, no kind picker, no title — a guardrail asserts there is nothing in its path that asks a question, because the moment a thought matters most is the moment it is most likely to be lost.
Like the rest of the Home surface, the Lab fills the pane (#620): the 780px centred cage is gone, and on a wide pane the thread list reflows into grid columns — a grid, not CSS columns, so the newest-first order still reads left-to-right rather than down each column. One column when the pane is docked narrow.
Four moves, and there will not be a fifth¶
| Fork | a variant that goes its own way. Both survive; neither is the parent. |
| Challenge | a thought that argues with another. Neither is marked right. |
| Connect | two clicks: arm on one, land on the other. Plain and untyped — never a semantic relation. |
| Leave | not a button. It is what closing does, and it costs nothing. |
Everything else — collision, constraints, perspectives, question transformations — belongs to the operator engine in phase 2. A fifth move here would start the feature collection this epic exists to prevent, and a test says so.
Phase 2 arrived with one operator, and the fifth move still does not exist. The collision (#559) puts two notes with nothing in common in front of you and asks what they could share; you answer in the same composer, and the thought it makes carries both notes — about gained a sibling, alsoAbout, rather than becoming a list, so the six readers that mean the one note this thread came from did not have to be retyped. A collision is an operator, not a move: fork, challenge, connect and leave are still four.
Thinking reads downward¶
An answer sits under what it answers.
The lab's first list was flat and sorted by clock, so a counterpoint you had just written appeared at the top, nowhere near the thought it argued with. That was not a layout problem with a layout fix: forkedFrom and challenges were two fields saying the same thing — which thought is this a response to — and nothing in the data said they formed a shape.
They do. A thought responds to at most one other:
That single parent is what makes the lab a set of threads, and a thread is the thing you actually want to re-read: I thought this, then I doubted it, then I found the flaw.
threadThoughts builds them, and its rules are the interesting part:
- Roots come newest first — the thread you were just in is the one you want.
- Children come oldest first — a conversation reads downward.
- A response whose parent was thrown away becomes a thread of its own. Nothing is ever dropped; losing a thought because its parent went is the worst thing this surface could do.
- A cycle, which only a hand-edited file can produce, is broken rather than recursed into.
A thread moves as one¶
Throwing away, setting aside and deciding against all act on the whole thread. An answer without the thought it answers is a fragment: if you discard the idea, the counterpoint you wrote against it has nothing left to argue with, so it goes too.
The tooltip says so before you click — takes 3 answers with it — because a destructive action that does not state its reach is how you lose four thoughts meaning to lose one. And the undo puts the whole thread back.
What is set aside is threaded too: a thread set down together should read together.
Connections are lateral, because a graph cannot nest¶
connect is undirected, can be many, and can cross threads — so it is not part of the tree. A connected thought appears as a chip beside the card, showing its first words; clicking one scrolls to it and flashes the card you landed on. A chip whose thought is gone says so rather than disappearing.
That is the split: responses nest, connections point.
Nothing is committed on a timer¶
A pause while writing is thinking, not a boundary. The first version of this surface saved a new thought on a debounce, and it was unusable: stopping for half a second turned half a sentence into a card, the surface rebuilt itself, and the cursor was gone.
So a new thought is written down only at a real boundary — Ctrl/Cmd+Shift+Enter, or leaving the box — and committing never rebuilds the surface: the card is inserted and the composer cleared in place. Editing a thought that already exists does save on a debounce, because there the file exists and nothing moves on screen.
The shortcut is Ctrl/Cmd+Shift+Enter, not plain Ctrl/Cmd+Enter: Obsidian (or a user hotkey) can swallow Mod+Enter before the box ever sees it, so the combo adds Shift to reach the composer reliably (#596). It commits what is in the box — the live text — never a cached copy.
There are two kinds of redraw, and confusing them was a real bug — an undo restored the file and nothing on screen, because the guard below refused while the composer held the cursor:
refresh() | for something that changed while you write. Refuses if a text box has focus. |
redrawAfterAction() | because you did something. Always happens, and puts the cursor back. |
The guard exists so typing can never move the ground under you. It is not a veto on what you asked for.
Two more rules hold it together, each stated in exactly one place:
- Nothing redraws while a text box has focus (
refresh()). - The draft lives outside the DOM, so even a redraw that does happen cannot lose it.
Fork and challenge do not create an empty card either: they arm the composer, so a relation costs a sentence instead of an empty file you have to go back and fill. And the actions fire on mousedown rather than click, because the composer commits on blur and a click that lands after a redraw is a click that never happened.
When you pick thoughts (to crystallize them together), the selection stays visible on each card — a standing accent, not something you see only while the pointer is over it (#596). You can see what you have gathered before you act on it.
On a touch screen (#868) a card's actions are always shown — a finger has no hover, and they used to be there but invisible, with the first tap spent on an emulated hover. Hold a card (half a second, without moving) to pick it; while anything is picked, a tap on a card picks or unpicks it, as a phone's lists do. A tap on its text, its buttons or its links still does what it always did. A mouse keeps the checkbox.
Leaving must never cost a sentence¶
Writing saves on a short debounce, on blur, and on close. A refuge that loses what you typed because you switched tabs is not one.
Throwing one away¶
A refuge you cannot tidy becomes a junk drawer, so a thought can be thrown away — and, like everything else ZettelFlow removes, it goes to Obsidian's trash, never to a delete.
The undo appears where the card was, not as a toast: this surface must not interrupt, and a strip in the space the card occupied is closer to where you are already looking. The thought is held in memory, so putting it back is instant.
Note the difference from decided against: that one keeps the thought, because a rejected conclusion is part of your intellectual history. Throwing away is for the typo and the false start.
Saying what the moves do¶
An icon is quick once you know it and opaque until you do. So every action carries a tooltip that says what it does rather than what it is called, and the header has a What do these do? legend — one sentence per move, closed by default, because a legend you cannot dismiss is clutter for everyone who already read it.
Colour distinguishes what a thought is to another: a fork takes the blue rule, a challenge the orange one. That is a distinction, never a verdict — neither side is marked right.
Feedback is deliberately small: a card fades in when it arrives, its rule pulses green for a moment when an edit is saved, and it slides out when thrown away. All of it respects prefers-reduced-motion.
It never counts¶
No total, no badge, no "you have 17 thoughts". A guardrail scans the whole of src/ — not just the Lab — for anything that counts thoughts, and scans this surface's strings for the vocabulary of debt. A counter would destroy the thing the Lab is for, and it would not be added in this file when it happened.
The move record (#491)¶
ZettelFlow records what you have — notes, states, links, verdicts, and every write it made on your behalf. Since 4.3 it also records what you did: the operation that changed your mind.
A move is five things and nothing more:
| Field | What it holds |
|---|---|
primitive | one of five: externalize · transform · perturb · explore · crystallize |
verb | one of eleven: capture · split · compress · reframe · challenge · counterexample · invert · branch · analogy · set-aside · crystallize |
subject | a vault path or a thought id — a reference, never content |
from | the move this came out of, so a branch has a genealogy |
because | one optional line, capped at 140 characters |
What it never holds¶
No note body, no thought text. The subject is a reference, exactly as Judgement.subject is, and the sanitiser rebuilds every entry on read so a field the type never declared cannot arrive from disk even if something wrote one there. That is what makes the record safe enough to keep on without asking.
Nothing records itself¶
Every move comes from a gesture you made. A log a vault event could append to is not a record of your thinking — it is telemetry, and this project deleted its telemetry on purpose. The rule is enforced the way the write seam is: the callers are derived from the source, and one that also listens to a vault event fails the build.
Bounded per subject, not by clock¶
The write record expires after a week because it carries values to restore. A move log that expired would destroy the thing it exists for, so it is bounded differently: the most recent 50 moves per subject, then a global ceiling of 2,000. The order matters — a global cap applied first would wipe the whole history of an idea you thought about once to make room for one you thought about all week.
The Lab's own gestures, written down (#492)¶
Four of the Lab's moves now record themselves, and nothing about the Lab changed to make that happen — no new key, no new button, no prompt:
| Gesture | Recorded as |
|---|---|
| fork | explore · branch |
| challenge | perturb · challenge |
| set aside (and decided against) | explore · set-aside |
| crystallize | crystallize, carrying the thought and the note it became |
A fork records when the response is actually written, not when you press F — arming the composer opens a box; the move is the thing you did, not the thing you were about to do. Setting a thread aside records one move, because that is one act however many cards it moves. And picking something back up records nothing: putting it back is not a new act of thinking.
Crystallizing writes both a judgement and a move, which looks like duplication until you say why. The judgement is the verdict — a human decided this chaos was an idea. The move is the operation — thinking became knowledge, here, out of that thought. One answers whether it was accepted, the other how it got here.
The lineage comes for free: the most recent move on the thought you acted on is what this one came out of, so a genealogy builds itself with nothing to maintain.
A move on a note (#493)¶
The moves are not a Lab feature. Challenging an idea, finding a counterexample and reframing it are exactly as useful against a permanent note you wrote last year, and two doors reach them:
- right-click — in the note you are reading, or on its tab or its file: Make a move…, which opens a keyboard-first picker. Arrows move, enter chooses, typing narrows, nothing has to be typed;
- Cultivate — one control on the note it is already showing you, opening the same picker. It shipped as eleven buttons in a row and was corrected in #509: eleven buttons is not a second way of offering the moves, it is the thing the picker exists to avoid, put back by hand.
Only where a note is knowledge (#496)¶
The entry appears only for a note inside the knowledge scope. An excluded path — a flow canvas, a script folder, the thinking space — never becomes an idea, so it never accrues moves. The log already refused them, and a refusal you cannot see is an invisible failure in a different place; better not to offer it.
The note is never written to. A move is a fact about what you did, kept in the log — no property, no frontmatter, no body. That is what makes it safe on a note you care about, and the write record is where you would see it if it were not true.
There are no move commands. 4.3 first shipped eleven of them and called the palette one gesture; it is one gesture for someone who already knows the feature exists, and twelve rows of clutter for everyone else. The palette is where you go looking for something you know is there, never where you discover anything (#496).
There is still no nested submenu: that needs MenuItem.setSubmenu(), which Obsidian ships at runtime and does not declare in its typings even at 1.13.1. This plugin already carries one unavoidable internal dependency — the canvas patcher — and a right-click convenience does not justify a second. One menu entry opening a picker is the next most native answer, and it is an interaction every Obsidian user already knows from the quick switcher.
The rule the picker has to satisfy is not "no modal" — it is that no move may ask you to justify or classify it. Naming the move is the one irreducible question, since there are eleven.
Two things this does not do, on purpose. It never asks why: the because line exists in the record and nothing prompts for it, because a gesture that opens a form is a gesture nobody repeats. And a move does not follow a rename — neither does a judgement, for the same reason: the alternative is a vault listener inside a log whose entire promise is that it never listens to the vault.
A move does something (#497)¶
Recording what you did was the cimiento, not the point. Since 4.4 a move on a note leaves you holding something:
| Verb | What choosing it does |
|---|---|
| challenge · counterexample · invert · reframe · compress · analogy · branch | opens the thinking space, framed, about that note |
| split | runs the atomicity split that has always existed |
| set aside | records, and nothing else — there is nothing else to do |
| capture · crystallize | not offered on a note: it is already captured, and already knowledge |
The vocabulary itself decides, which is why the picker stopped offering crystallize on a note.
The system provides the frame; you provide the content. invert does not produce "what if B causes A?" — a generated formulation is interpretive output needing a §XII gate, and it would be the worse product, because your inversion is the thinking. The one exception is not a generation: an operation may perform a transformation already derivable from what you wrote, and splitting a note at its headings invents nothing.
A move you did not make is not a move. A framed verb records when the thought is written, not when the space opens: a composer you opened and closed is not an act of thinking. The move then names both ends — the note it was about, and the thought it produced.
There is no field for why, and there was one until 4.4. The question where do I say what my challenge was? is the right one, and a capped line in a log is the wrong answer: the reason is a thought, and it deserves to branch, survive and come back.
How you got here (#494)¶
The moves are read where the question already lives: the Evolution timeline, which already merged what changed (snapshots) with what you ruled (judgements). Moves are the third strand — what you did — and timelineEvents takes them as a default-empty third argument, so a note with no moves renders exactly as it did before.
The strand shows even when snapshot recording is off. The timeline is opt-in because it stores claim texts; a move stores none, so the reason for the opt-in does not reach it — and a user with snapshots off would otherwise have moves and nowhere to read them.
A move that produced something names it and opens it — "challenged → a counterpoint" — so the line reads you did something and here is what came of it. A thought since discarded is named and not linked: the Lab is a place things are deliberately thrown away, and a dead link is worse than a plain fact.
You can forget a move from the line you are looking at. It touches the log and nothing else.
What it never says¶
A run of moves invites a conclusion, and the interface does not get to draw one. No density, no depth, no "well developed", no "this thread stalled". Counting transformations is still counting, and the manifesto refuses productivity metrics. A locale scan in both languages enforces it, because this is the single most likely place for a helpful sentence to arrive in six months and a note in a review will not stop it.
Crystallize — the only door¶
Pick out the thoughts that turned out to be an idea, and Crystallize. A note appears in your real graph, in the real model, with everything a note has.
The archaeology survives¶
Zettelkasten rewards thought that is already clean: a note arrives with a title and a claim, and the twenty minutes of contradiction that produced it are gone. Here the note keeps both halves of its provenance:
The idea, as you wrote it.
## Born from
- "maybe the problem isn't that AI thinks worse than us"
- "or maybe that's a simplification"
- "what if the real problem is speed"
Links and frozen quotes. Links let you navigate back while the thoughts exist; the quotes are plain text, so the record is still readable after you empty the Lab. A record that points at a deleted file answers nothing — the same reasoning that makes the write record keep previous values rather than a pointer.
Quotes are clipped and capped, and a capped one says and 4 more rather than silently dropping them.
It is the §XII verdict¶
The constitution requires that interpretive output reach the vault only through an explicit human accept/modify/reject, recorded. Everywhere else in ZettelFlow that is a defensive guardrail on AI and heuristics. Here it is the mechanic. The Lab is where interpretation happens; this is the gate it passes.
So the guardrail is about who may open the door: a test asserts crystallize() has exactly one caller, that it sits behind a button you press rather than a lifecycle hook, and that neither it nor the modal touches AI. The verdict goes to the same JudgementLog as every other — subject only, never content.
Nothing is consumed¶
Crystallizing does not delete, move or lock the thoughts. The same chaos can produce a second idea next month, and a door that eats the room behind it is not a door. A test asserts the applier cannot reach a delete.
The title and the body are proposed, never imposed — a proposal that is hard to change is an imposition. Cancelling writes nothing at all.
Through your crystallize flow¶
By default a new note lands at the vault root. Give a canvas the Crystallizes thoughts role (Settings → Your flows, see flow roles) and a new note goes through it instead: the preview says This note continues in «flow», and its button reads Continue in «flow». Confirming opens that flow — an ordinary one — with the title you accepted and the crystallized content already in its preview, so its steps choose the folder, the template and the properties as for any note.
- The content goes at the top of the note's body: your text, Born from, and the
source::lines. A step's template can place it elsewhere with{{crystallize.content}}(exactly once);{{crystallize.title}},{{crystallize.quote}}and{{crystallize.source}}carry the rest. In any other flow the four are empty. - Nothing is written on cancel. Close the preview or the flow and no note exists, no verdict is recorded, no draft is kept, and the thoughts stay where they were.
- The verdict is recorded when the flow builds the note — the same
crystallize:judgement, the same single undo, and the thoughts set aside, exactly as a direct crystallization. - Your words arrive as text. A thought that says
<% … %>is written as<% … %>, so Templater (which runs on a note a flow builds) never executes it. - A flow that cannot be opened says so and writes nothing — it never falls back to the root. Going back into the note a thread was about is still a plain append, never a flow.
Set it aside¶
Some ideas should not be processed. Not developed, not linked, not classified, not turned into a note, not resolved — just left alone, without that meaning abandoned.
Two reasons, one state:
| Set aside | not now. |
| Decided against | you rejected it — and it is still kept, because a conclusion you rejected is part of your intellectual history, and the context that made it wrong can change. |
Why this is in phase 1¶
Every tool's answer to "leave this alone" is an inbox, and an inbox is a debt: a list that grows, counts itself, and greets you with how far behind you are. Without this, the Lab would be another inbox. That is why it shipped alongside the space rather than after it.
The rule, and the test that is the feature¶
Nothing here ever counts at you. No total, no badge, no ribbon number, no status bar, no notification, no reminder, no "you have 17 waiting". Coming back is your move.
test/application/thinking/noDebt.test.ts is where most of this feature's weight lives, because the requirement is negative. It scans all of src/ — not just the Lab — for anything counting thoughts, and the Lab specifically for badges, status-bar items, intervals and scheduled returns. Then it scans the strings for the vocabulary of debt: pending, overdue, waiting, remaining, backlog, inbox, due, should, must, still.
A counter would destroy the thing the Lab is for, and it would not be added in incubation.ts when it happened — it would be added in a ribbon, or in Home. Hence the scope of the scan.
What you set aside sits behind a door, not a queue: closed by default, labelled with a name rather than a number.
Being welcomed back¶
When you reopen something, it says what you stopped at and what has been written since — notes created after that moment that share a word with what you were stuck on, in the order they were created.
Mechanical, and deliberately dull. "This appeared since" is a fact; "this is now promising" is a judgement, and it is yours (§XII). A test asserts appearedSince sorts by creation time and that the module's code contains no notion of score, rank, relevance or weight.
Think before you look — moved to Explore (#576)¶
This lived in the Lab's header until epic #574, as one eye-off button among ten identically weighted controls. The person who specified it could not find it in their own vault, which is the measurement that started the epic.
It now lives where you ask — a control in Explore, off by default and remembered once you turn it on. Nothing about what it does changed; what changed is that a second query engine came out with it, and the question you type is run through the about: predicate the product already had.
See Ask your graph → think before you look and capability doors.
Arriving from somewhere (#473)¶
Cultivate and the Lab are not the same thing wearing two hats, and that was argued out carefully enough to write down:
| Cultivate | The Lab | |
|---|---|---|
| The subject | a note that exists | the thought; there is no subject yet |
| What you bring | something you already know | not knowing what you think |
| Where output lands | the note, which is how a note matures | nowhere, until you crystallize |
A counterpoint to note X is a fact about X, and putting it into X is knowledge evolves, not premature commitment. A shared verb is not a shared capability — delete exists on notes and on thoughts, and nobody calls that duplication.
What was missing was a door, in both directions:
- From a note — a command, and an item in the file menu.
- From Cultivate — I do not know yet — think in the lab, the exit for the one case that surface cannot serve.
The note comes across as the thread's subject, carried in the view state through the same deep-link seam everything else uses. Every thought written in that visit inherits it, so an answer you write an hour later still knows what it was about.
Crossing writes nothing¶
Leaving a question unanswered is not an edit. A guardrail asserts the bridge reaches no writer at all — a bridge that dirties your vault is a bridge nobody crosses twice.
The subject is named, not shown¶
The thread's root says which note it is about. It does not render the note's content: that would make the Lab a reading surface, and put the note back at the centre of a place that exists for the thought. Since #874 a click on the name shows only that source, and the note opens from the source's header (Thinking by source) — one click further, never doubled. A subject that has since been deleted says so, and the thread is still yours.
Going back where you came from (#474)¶
A thread with a subject was a one-way street: you crossed over to think, worked something out, and the only thing crystallization knew how to do was create a new note — leaving the one you came from exactly as unfinished as when you left it.
So crystallization now has two destinations, and when both are open neither is the default. Where a piece of thinking belongs is the decision; guessing it is how it ends up in the wrong place.
Going back appends, never rewrites:
## From the lab
What you wrote, as you edited it.
## Born from
- "maybe the volume does not justify it"
- "or maybe it does at 10x"
Whatever the note already said is untouched, the write goes through the recorded seam so it can be taken back (#454), and the verdict goes to the same log as every other — naming the note it landed in.
If the thread's subject has since been deleted, only the new-note destination is offered, and the reason is said. Offering to append to something that is gone is offering to fail.
A crystallization of thoughts that disagree about what they are about has no subject: there is no honest single note to return to.
Capturing lands here (#475)¶
The capture command used to write Inbox/<title>.md with state: fleeting. Three commitments before you had decided anything: that it is a note, that it has a title, and that it has a lifecycle state. It was immediately in the knowledge model, could be an orphan, counted in Health, and was one more thing in an inbox to get through.
An impulse has no subject. It now leaves a thought.
Same command, same hotkey, same single prompt — two commands doing almost the same thing is what subtraction refuses, and the friction was the feature. A captured thought is indistinguishable from one typed in the Lab: there is no "captured" flavour to manage later.
With no Lab folder configured it says so and writes nothing. Falling back to creating a note is how you end up with the thing this change exists to stop.
QuickCaptureService was renamed rather than removed: its remaining caller is inquiry (#401), whose outcome is a frozen reviewed snapshot and is not capture. It is now CreateOnlyWriter, which is the job it actually has.
From the keyboard (#476)¶
Every move was a button that appears on hover. That is fine for a surface you visit; it is wrong for the place an idea starts — reaching for the pointer mid-sentence is the same interruption as being asked for a title, except it happens every time instead of once.
J / K | next · previous thought | F | fork |
C | challenge | L | connect |
X | pick out | Y | crystallize what you picked |
S | set aside | Esc | stop writing |
Shift+A | decided against | Shift+D | throw away |
Four rules make single letters safe in a surface whose whole purpose is typing:
- Writing wins over shortcuts, always. Inside a text box, a letter is a letter. Only
Escgets through. - Nothing destructive on a bare key. Throwing a thread away needs
Shift, because a stray keystroke in a place you were told is safe must not be able to do it. - Scoped to the view. A single letter that worked everywhere in Obsidian would be a bug in somebody else's workflow, and a modifier is left to Obsidian entirely.
- The focused thought is visible, not guessed — and its actions stay on screen, because you cannot hover what you reached with a key.
One table (application/thinking/labKeys.ts) is read by the handler, the tooltips and the legend, so what a key does cannot drift from what the legend says it does. A guardrail asserts every rendered action is wired to it: a move added later cannot be mouse-only.
When the lab has grown (#477)¶
The Lab works because it asks nothing of you, which is also what makes it fill up. After a few weeks the thread you want is below the fold, and the refuge is a wall of text.
Every obvious fix is the wrong shape. A list of what to process is an inbox. A count is a debt. A ranking of what looks promising is a judgement, and it is yours (§XII).
What is actually needed is narrower: a way to find the thing you are looking for, when you are looking for it.
- A filter. Empty by default, it narrows and never reorders.
- A match keeps its thread. If an answer matches but its question does not, the question comes with it: an answer alone is a fragment.
- Case- and accent-insensitive, because you will not remember whether you typed análisis or analisis at eleven at night.
- Folding, so a long thread is one card when you are not in it. View state: it survives a redraw and not a restart, because nothing about how you looked at a thread belongs on disk.
- The Lab opens where you left it.
No saved filters — one you keep is a queue with a different name — no suggestions, no counts. The guardrail from #469 is extended over all of it, and lab.thread.500 measures a five-hundred-thought lab in CI: 0.65 ms per keystroke, against a ceiling of 8.
Finding a thought (#596)¶
The filter used to appear only once there were enough thoughts to lose one in. That threshold was the wrong idea: the find bar is now always there, quiet and empty, because it also carries the tags — and a control that comes and goes is one more thing to learn. It is renderFindBar, one bar that does three things.
- It searches what you wrote. Free text runs over the thought's body, the inline
#tagsinside it, and the subject note's name when the thread came from one — so the thing about that note is findable by the note. And it narrows to the books, PDFs and notes you pick with From… or a card's from line (Thinking by source). #tags, written inline, become chips. A#ideain the body of a thought shows as a chip on the card; clicking it adds it to the find bar as an active filter, and a thought must carry all the active tags to show. The tags are parsed back out of the body on every render — there is no registry of them to keep in sync, and a stored list of your most-used tags is a count wearing a coat.- Search reaches the room you set things down in. When a query is running, the set-aside room opens itself to its matches under matching set-aside thoughts — a phrase for what you are looking at, still never a number for how full it is.
/ puts the cursor in the find bar; Esc returns you to the list — the find bar is in the one key table (labKeys.ts) with every other move, so it cannot be mouse-only. It is not, itself, an act of thinking: LAB_MOVE_VOCABULARY records find as null, the same as next, previous and leave — getting back to a thought is not a move on it.
The #469 guardrail grew to cover all of this: searchThreads may never rank, a tag chip may never carry a number beside it, and the strings the tags and the find added are scanned for the vocabulary of a queue.
The list is time, and a calendar to move through it (#596)¶
Time is the honest default order, and it always was — roots come newest first, the thread you were just in at the top. What changed is that the list stopped rendering all of them. A year of thinking is thousands of thoughts, and putting all of them in the document is DOM weight, not compute: the model stays cheap (lab.thread.500 measures a five-hundred-thought lab at 0.65 ms per keystroke). So the list is now a window.
- Infinite scroll. The newest
LAB_PAGEthreads render; scrolling toward the foot brings the next page, appended in place so your position never jumps. The window is a pure function (labPage.ts,windowOf) that slices and never reorders — and the one number it could carry, how many are left, it does not: what waits below is a boolean,hasMore, nothing more. - The foot is a door, not a number. A quiet older thoughts affordance sits under the window — a click loads the next page, so the list works without a scroll wheel. It never says "23 more": that is the backlog count the Lab exists to refuse (#469).
- A calendar is the other way in. A toggle in the find bar opens a small month grid; its header steps by month, and clicking it opens a year of months to pick from — so you can reach a day, a month, or a year. A day (or month) that holds thinking carries a dot: presence, never "14 on Tuesday", because counting what you wrote per day is exactly the productivity report the Lab is not. Clicking a marked day jumps the list to that day's newest thread and gets out of the way.
- Local, not UTC. The day a thought belongs to is the day you were in when you wrote it, so a thought at 1 a.m. stays on its day. The buckets (
labCalendar.ts) areSets — membership is the whole of what they say — and they reflect the current find query, so finding and the calendar compose: filter to a tag, and the calendar marks only the days that tag appears.
The #469 guardrail reaches all of it: the window exposes hasMore and no remaining count, the foot renders no total, the calendar cells carry a dot and never a tally, and the strings the window and calendar added are scanned for the vocabulary of a queue.
Thinking by source (#874)¶
What the Reader pours into Think — highlights, margin notes, ink, the answers to its moves — is all tied to a book, a PDF or a note. Think can now be read one source at a time, in the source's order: everything you thought while reading that book, chapter by chapter, the way you read it.
- Three doors, all where you already are.
- A card's from line — about X, Highlighted in X, From X, an ink note's source — is a door: Only this source. With a source already picked it reads Add this source.
- From… in the find bar opens the sources your thoughts came from: recent first, each with its kind (book, PDF, note) and a quiet when, with a field that narrows them by name. A row toggles its source and the picker stays open, so two books are two clicks. It never says how many thoughts a source holds.
- Arriving from the Reader — Open in Think on a highlight, Think on what you marked at a source's end — opens Think already filtered to it, in reading order. Every other arrival (Think about this note, a move on a note) keeps the Thinking about X line as before.
- Several at once. Each picked source is a chip in the find bar, in the order you picked it. Sources are OR between them and AND with the text, the tags, the meaning and the calendar — one query, which the set-aside room follows too. A thread from a source is shown whole.
- In reading order, by default. Whenever a source is picked, the list reads the source's way: the picked sources one after another, a head before each; inside a book or a PDF by chapter (then print page), oldest first at the same place, the threads tied to no place last; a note by date. Newest is the order of the rest of Think. Reading order is a separate step you chose, applied before the window — the find itself still never reorders or ranks (#469).
- One source picked draws its header — the cover the Library already drew this session, or a plain tile; its name; Book, PDF or the note's folder — and the one way to it, Open in the Reader or Open the note. That is where opening a note lives now: the from line is the filter. A new thought you write is about that source; with two or more sources picked it is about nothing in particular, never one of them chosen for you.
- The spine. One book or PDF, in reading order: a rail beside the list with a tick where each thread sits — where you thought, and how close together — with no number on it. A tick brings its card into view, growing the window if it is further down, and the card says so once.
- Taking a source away: ✕ on its chip, Backspace in the empty find field (the last one), or Esc — nearest first: the picker, then the text box you are in, then the sources. Clear clears them with the rest.
- One continuous shot (§XVI). The source's name lifts off the line you touched and becomes the chip; the cards the filter leaves out slide off to the side; the ones that stay close up into their places. Taking a source away plays it backwards.
transformandopacityonly, any input jumps it to its end, and under reduced motion the list simply changes.
With a source picked the calendar marks every day its thoughts were written, and a click on a day goes to the first card in the list holding a thought from it. The pure parts — the sources, reading order, the spine — are application/thinking/labSource.ts; the #469 guardrail scans them, the picker and the new strings for anything that counts, and lab.thread.500 times a lab read by two sources at 0.63 ms per keystroke.
What is not here yet¶
Phase 2 of epic #465: the operator engine (collision · constraints · perspectives · question transformations, as one mechanism with four vocabularies), the shadow graph, and tension and belief as objects.
The name, and the one in the code¶
The surface is called Think (#479). It was Lab, which reads as somewhere advanced you go once you already have something to test — the opposite of an invitation to begin, and beside Home · Cultivate · Recent it was the entry a new user was least likely to open first. A verb is the invitation, and it is the only verb the ribbon menu offers.
It leads that menu, above Create note. That order is the claim: an idea starts as a thought, and a note is what it may become.
The code still says lab — the classes, the lab/ folder, thoughtLabPath. That is deliberate. A product name and a code name serve different readers, lab remains an accurate description of what the code is, and renaming identifiers for a label is churn with real regression risk. The repo already does this: historyView serves the Recent mode. What matters is that the command id and the mode id never changed, so nobody's hotkey or deep link broke.
Capability disclosure¶
| Capability | Used |
|---|---|
| File system — write | Notes inside the Lab folder you choose |
| Network | No |
| Clipboard | No |
| Script execution | No |
| AI | No |
README vocabulary for this page: Cognitive moves.
Highlights by what they mean (#720)¶
A highlight from the Reader shows what you marked it as (Idea, Question, Quote or To discuss) as a chip on its quote, in the same colour as in the book. Click the chip to show only the highlights with that meaning; the find bar shows it as a chip you can remove. Like every filter here, it narrows and never reorders.