Thread Shell

ENOK Foundry · the conversation spine
The thread — grouped, dated, streaming, answerable
The scroll law. Scroll up a few messages, then press Send 3. The thread does not move — a reader who has scrolled away is never yanked. The arrivals are counted into the 3 new messages pill instead, and one tap returns you to the end. A reader who was at the bottom is simply carried along.

Waiting is not a tally. The asks waiting pill in the header counts InChat.unresolved() — the callouts themselves are the source of truth, so the pill can never drift from the transcript. Tapping it jumps to the first thing waiting on you, and on a consent gate it lands on the deny side, never the yes.
Event log
waiting for something to happen…
The whole component — two files
<link rel="stylesheet" href="thread-shell.css"> <script src="thread-shell.js"></script> <div class="th-root" data-th-theme="light"><div id="thread"></div></div> <script> var thread = ThreadShell.mount('#thread', { title: 'ENOK', sub: 'Ora · claude-opus-5', you: 'Daniel', ai: 'Ora' }); thread.append({ author:'Daniel', role:'you', text:'Where did the staging run land?', at:new Date() }); // A stream is ONE message that grows — not one message per token. var s = thread.stream({ author:'Ora', role:'ai', at:new Date() }); s.push('Five files staged, '); s.push('catalog updated.'); s.done(); // Callouts ride inside a message; the shell counts what is still waiting. thread.append({ author:'Ora', role:'ai', text:'Two things need you.', nodes: [ InChat.create({ id:'lane', kind:'ask', title:'Which shelf?', options:['candidate','live'] }) ] }); </script>