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
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>