Something went wrong. Try again.
A collaborative coding-agent orchestrator for atproto radl.app
Something went wrong. Try again.
TypeScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697// Reading a thread back, for a terminal.//// Every other read path in the product is the UI. That was survivable while a thread was something// humans wrote and agents only read — but `message post --parent` and `request create --type answer`// both take a message's pinned `uri#cid`, and until now the only way to learn one from the CLI was// to open the daemon's sqlite store and eyeball `value_json`. A command that writes a reply is not a// CLI surface if finding what to reply to needs a database client.//// The exchange itself is core's `threadRows` — the same fold the UI draws — flattened back to one// line per message so the output stays greppable and `cut -f1`-able. Structure survives the// flattening in the ORDER (a row, then its replies) and in the `reply` flag, not in indentation:// indenting column one would put whitespace in front of the locator, which is the one column a// script actually wants.
import { threadRows, type ThreadMessage } from '@radial/core'import type { GoalView, ProjectView } from '@radial/core'
/** What `message list` knows about one message. `--json` prints these verbatim. */export interface ThreadLine { /** The pinned `at://…#cid` locator `--parent` and `--subject` take. */ locator: string /** The author. DIDs, not handles: a record stores the DID, which outlives any handle. */ did: string createdAt: string body: string /** 0 for a top-level message, 1 for a reply. Core's fold nests one level and no further. */ depth: number /** When this reply answers another reply rather than the row it is filed under. */ inReplyTo?: string /** The request this message is about, when it carries `re`. */ re?: string /** A message carrying `re` that is not itself a reply: what a turn wrote back to its requester. */ question: boolean /** …and nobody has answered it, so the request it names is parked. This is the one to reply to. */ unanswered: boolean /** The turn refused the request outright rather than asking about it. */ declined: boolean}
/** * One line per message in the target's thread, top-level messages in time order and each one's * replies behind it. * * `unanswered` is read off the view's own `awaitingInput` rather than recomputed here: that set is * the fold's answer to "which requests are blocked on a person", and a second implementation of the * rule in a CLI is exactly how a command comes to disagree with the daemon it is meant to explain. A * question is attributed to its request, so two questions on one parked request both read * `unanswered` — the same approximation the UI's thread makes, and it errs toward showing a reply * target rather than hiding one. */export function threadListing(view: GoalView | ProjectView): ThreadLine[] { const awaiting = new Set(view.awaitingInput) const line = (message: ThreadMessage, depth: number, inReplyTo?: ThreadMessage): ThreadLine => { const re = message.value.re?.uri const question = Boolean(re) && message.value.parent === undefined return { locator: `${message.uri}#${message.cid}`, did: message.did, createdAt: message.value.createdAt, body: message.value.body, depth, ...(inReplyTo ? { inReplyTo: `${inReplyTo.uri}#${inReplyTo.cid}` } : {}), ...(re ? { re } : {}), question, unanswered: question && message.value.declines !== true && awaiting.has(re as string), declined: message.value.declines === true, } } return threadRows(view).flatMap((row) => [ line(row.message, 0), ...row.replies.map((reply) => line(reply.message, 1, reply.inReplyTo)), ])}
/** The flag column: what this message is, in the words the docs and the UI use for it. */export function threadFlags(line: ThreadLine): string { const flags = [ ...(line.depth > 0 ? ['reply'] : []), ...(line.question ? ['question'] : []), ...(line.unanswered ? ['unanswered'] : []), ...(line.declined ? ['declined'] : []), ] return flags.length > 0 ? flags.join(',') : '-'}
/** * One tab-separated row: locator, author, time, flags, and the first line of the body. * * Truncated the way `type list` truncates a brief — enough to recognise which message this is, on the * understanding that reading it in full is what the UI and `--json` are for. */export function formatThreadLine(line: ThreadLine, width = 80): string { const first = line.body.split('\n')[0] ?? '' const summary = first.length > width ? `${first.slice(0, width - 1)}…` : first return [line.locator, line.did, line.createdAt, threadFlags(line), summary].join('\t')}