SLOPSHOPPER

herald

Herald: news and releases for your stack, right above the prompt.

new
v0.1.2MITupdated 2026-10-08dieg666/claude-herald
A shopper browsing a rack in a slop shop
Preview could not run: TypeError: undefined is not an object (evaluating 'exports_state.INITIAL_STATE.sources') at anonymous (file:///inner.js:1714:104) at run (inner.js:982:9) at /home/runner/work/slopshopper/s
README

Herald

News and releases for your stack, right above the prompt.

Claude Code 2.1.287 or later License: MIT

Herald is a Claude Code mod, a plugin that draws its own interface inside Claude Code. It keeps you up to date on Claude, the tools around it and the packages your project depends on, without leaving the terminal. A band above the prompt rotates the latest headlines and flags the releases of your dependencies that are breaking or security fixes.

The Herald band above the Claude Code prompt. The first page shows two npm packages flagged with a warning sign for breaking releases, the next package from 14.2.15 to 16.4.0 and jsdom from 25.0.1 to 30.1.2, and a Hacker News headline. The band then turns to the next page of news and releases.

Install

You need Claude Code 2.1.287 or later. Run these in your shell:

claude plugin marketplace add dieg666/claude-herald
claude plugin install herald@claude-herald

If a session is open, run /reload-plugins in it. Then run /herald to open the pane. The band appears once the first refresh has fetched items, and releases of your dependencies join it once Herald has read their feeds.

To try Herald for one session without installing it, or to update or uninstall it, see How-to guides.

What you get

A band above the prompt

The band shows three items a page and turns the page every 20 seconds. It mixes every enabled source so that a busy one cannot fill a page, and it puts the newest flagged releases of your stack first. Each row shows the source, the item's age and a headline that links to the item. Items you have opened or copied leave the band.

KeyWhat it does
p, nShows the previous or next page.
aPauses or resumes the rotation.
k, jSelects the item above or below.
oOpens the item in your browser.
sWrites a 3 to 5 line Haiku summary under the item in the transcript.
vSaves the item for later.
cCopies the item for Claude, as a prompt to paste.

Hotkeys work while the band or the pane has the keyboard: press ctrl+x tab or click it (see Keyboard focus). On a narrow terminal the band turns compact and shows one headline a page.

A pane for every source

/herald opens a pane with an All tab, a tab per source with a count of new items, a Your stack tab and a Saved tab.

The Herald pane. It opens on the All tab, moves the selection, switches to the Hacker News tab, then to the Your stack tab, and back to All.

All lists the band's items in the band's order and keeps the ones you have read, dim. Press l for All, 1 to 9 for the source tabs, y for Your stack and 0 for Saved.

Release tracking for your stack

Herald reads your project's package manifests and lockfiles, finds each package's GitHub repository through its registry and follows its releases. It compares each release with the version you use and marks it ⚠ when it is flagged breaking or security, by keywords and by Haiku reading the release notes.

The Your stack tab: five npm packages behind, three of them marked with a warning sign for breaking releases, each with its current version, the newest version, the level and the date.

It reads npm, Python, Go, Rust, Ruby, PHP, .NET, Java, Swift and Dart projects. By default the band and the pane show minor, major and flagged releases, and only breaking and security releases raise a toast (Show and toast levels). Run /herald deps to see which packages it follows, and /herald deps ignore <package> to stop following one. For the rest, see Your stack.

Sources to start with

Herald starts with ten sources, nine of them on: Anthropic news, the release feeds of Claude Code, the Claude Agent SDK (TS), the Anthropic Python SDK and the MCP spec, Hacker News, Simon Willison, AINews (smol.ai) and the GitHub changelog. The tenth, Claude status, is off. You can turn any of them on or off and add your own RSS or Atom feeds and web pages.

Privacy at a glance

Herald asks Claude Code only for what its features need:

  • It never reads your prompts, your conversation or what Claude does in your session.
  • It reads only package manifests and lockfiles from your project's files, such as package.json and uv.lock. Files stack detection reads has the full list.
  • It sends package names to the package registries, GitHub and a few Go import hosts to find releases (Network hosts). For NuGet and Maven it also sends the version you use. To stop that for a project, turn Your stack off with /herald deps off.
  • It fetches the news sources you have on, and the addresses you add yourself.
  • It uses a little Haiku through your own Claude Code session, to check releases and to read web-page sources. Automatic summaries are off.
  • It has no telemetry. Nothing goes to the author.
  • Its data stays on your machine, in a JSON file in Claude Code's plugin store. To delete it, see Delete stored data.
  • Feed text is treated as untrusted. Copy for Claude puts text on your clipboard but never submits a prompt.

For the exact calls, hosts, files and accepted risks, see Security and privacy details.

What Herald sends, runs and hooks

What it sends and where. Fetch requests go to the news sources you have on: www.anthropic.com, github.com (Claude Code, Claude Agent SDK, Anthropic Python SDK and MCP spec releases), hnrss.org (with news.ycombinator.com as a fallback), simonwillison.net, news.smol.ai and github.blog, plus status.claude.com if you turn Claude status on, and any feed or page address you add. While Your stack is on, package names go in the URL path to the registries of the ecosystems your project uses (registry.npmjs.org, pypi.org, crates.io, proxy.golang.org and a few Go import hosts, rubygems.org, repo.packagist.org, api.nuget.org, repo1.maven.org), and repository paths go to github.com for their releases.atom or tags.atom feed. Item and release text goes to Haiku through your own Claude Code session. Nothing else leaves your machine. Network hosts has every host and when it is contacted.

What it runs and why. It runs only fixed programs, each with an argument vector and no shell of its own, to open a link in your browser and to copy text for Claude:

  • uname -s, to tell macOS from other systems.
  • open (macOS), rundll32 url.dll,FileProtocolHandler (Windows) or xdg-open (elsewhere, started through sh -c with the address as $1), to open an http or https link.
  • pbcopy (macOS), powershell and clip.exe (Windows), or wl-copy and xclip (elsewhere), to put text on the clipboard. The text goes to the program's standard input.

Commands the mod runs has each exact command.

What it reads from your machine. Package manifests and lockfiles in your project, the environment values HOME and USERPROFILE (to stop the project search at your home directory) and OS (to tell Windows apart), and Claude Code's language setting (for the language of summaries). It asks for no secrets.

What each hook does. It has no hook on prompts, tool calls or the transcript.

  • session.start loads its stored data, starts the refresh and rotation timers and registers /herald.
  • command.run for herald answers /herald and its subcommands.
  • ui.render above the prompt draws the band; it adds the band to what other mods draw there and changes nothing else.
  • ui.render for the herald pane draws the pane.
  • ui.open and ui.close for the herald pane redraw the band, which steps aside while the pane shows; they change nothing in the pane itself.
  • SessionStart on clear, resume or fork reloads the stored data and registers /herald again, because those reset the mod's state.

First commands

CommandWhat it does
/heraldOpens the pane.
/herald depsShows which packages Herald follows for this project and where each one's releases come from.
/herald deps ignore <package>Stops following one package, such as npm:left-pad.
/herald deps toast offStops release toasts.
/herald deps offStops following this project's dependencies and leaves the other sources alone.
/herald disable Hacker NewsTurns a source off. /herald list shows the names.
/herald add <url> [name]Follows an RSS or Atom feed. /herald add-page follows a web page.
/herald interval <minutes>Sets the minutes between refreshes, 5 by default.
/herald rotate <seconds>Sets the seconds between band pages, 20 by default.
`/herald lang <feed\user\code>`Sets the language of summaries, such as es.
/herald summaries onWrites a one-line Haiku summary under each headline. Off by default; s summarizes one item on demand either way.

For every command and setting, see the Reference.

FAQ

Does Herald send my code anywhere? No. It reads only package manifests and lockfiles from your project's files, and it sends package names to find their releases. To stop that for a project, run /herald deps off.

What does it cost? With automatic summaries off, Herald makes a few small Haiku calls per refresh, with a cap: it checks new releases of your stack and reads a page source's headlines when the page changes. Every answer is cached. The calls count against your plan or are billed to your API key. For the caps, see Haiku calls and cost.

Why does a release not show up? It might be at or below your version, an unflagged patch release (hidden by default), a pre-release, or waiting for its lookup. For the checks, see Fix common problems.

How do I remove Herald and its data? See Uninstall and Delete stored data.

Why is the plugin herald and the marketplace claude-herald? claude plugin validate refuses plugin names that start with claude-. For other design choices, see Design decisions and deviations.

Documentation

  • How-to guides: install, update and uninstall, delete stored data, reduce Haiku calls and fix common problems.
  • Band and pane: what the band and the pane draw, their keys and the item actions.
  • Your stack: which packages Herald follows, how it finds and classifies releases, and the /herald deps subcommands.
  • Reference: terms, commands, sources, templates, summaries, Haiku calls, stored data and limitations.
  • Security and privacy details: hooks, calls, hosts, files read, commands run, telemetry, untrusted content and accepted risks.
  • Privacy policy: what Herald reads, sends and stores, in short.
  • Design decisions and deviations: why Herald behaves as it does.
  • Development: run the checks and change the mod. README media says how the screenshots and recordings are made.

Requirements

  • Claude Code 2.1.287 or later. Tested with Claude Code 2.1.293.
  • A terminal or the Desktop app to draw the band and the pane. In other places, such as claude -p, VS Code and cloud sessions, /herald answers with the latest items as text and /herald deps lists your stack as text.
  • A session that can call a model. Page headlines, release checks and summaries use Haiku through your plan or API key. Herald asks for one-line summaries on its own only while automatic summaries are on; they are off by default.

License

MIT, see LICENSE.

Source 91 files
hooks/register.tsx 501 lines
1import type { EngineInterface, Register, UiPressArgument } from 'claude-code'
2import { atom, read, update } from 'claude-code'
3
4import type { Item } from '../types/index.js'
5import Actions from './actions'
6import Band from './band'
7import Commands from './commands'
8import Stack from './deps/stack'
9import type { Host } from './host'
10import Names from './names'
11import Pane from './pane'
12import Refresh from './refresh'
13import State from './state'
14import Store from './store'
15import Summaries from './summaries'
16
17const SOURCES = atom({ plugin: 'herald', key: 'sources' } as const, State.INITIAL_STATE.sources)
18const SETTINGS = atom({ plugin: 'herald', key: 'settings' } as const, State.INITIAL_STATE.settings)
19const ITEMS = atom({ plugin: 'herald', key: 'items' } as const, State.INITIAL_STATE.items)
20const SAVED = atom({ plugin: 'herald', key: 'saved' } as const, State.INITIAL_STATE.saved)
21const SUMMARIES = atom(
22  { plugin: 'herald', key: 'summaries' } as const,
23  State.INITIAL_STATE.summaries,
24)
25const BAND = atom({ plugin: 'herald', key: 'band' } as const, State.INITIAL_STATE.band)
26const PANE = atom({ plugin: 'herald', key: 'pane' } as const, State.INITIAL_STATE.pane)
27const STATUS = atom({ plugin: 'herald', key: 'status' } as const, State.INITIAL_STATE.status)
28const STACK_STATE = atom({ plugin: 'herald', key: 'stack' } as const, State.INITIAL_STATE.stack)
29const READ = atom({ plugin: 'herald', key: 'read' } as const, State.INITIAL_STATE.read)
30const VIEWED = atom({ plugin: 'herald', key: 'viewed' } as const, State.INITIAL_STATE.viewed)
31
32// Bounds the mod's model calls for summaries, at most two in flight; other model work may share it.
33const MODEL_LIMITER = Summaries.limiterOf(Summaries.SUMMARY_LIMITS.concurrentRequests)
34
35// The summary requests in flight and the queue ordering their writes, for this load of the module.
36const SUMMARY_JOBS = Summaries.summaryJobsOf(MODEL_LIMITER)
37
38// Following the stack's releases for this load of the module: the start detection, the refresh in flight, recent failures.
39const STACK = Stack.stackLoopOf()
40
41/**
42 * What the band and the pane run for the items they show: one-line summaries for news items while automatic summaries are on, the model's flag check for stack items.
43 *
44 * @param host the engine
45 * @param items the items shown
46 * @param signal aborts the model calls
47 */
48function showItems(host: Host, items: readonly Item[], signal?: AbortSignal): Promise<unknown> {
49  return Promise.all([
50    Summaries.ensureVisibleSummaries(
51      host,
52      SUMMARY_JOBS,
53      items.filter(item => !Stack.isStackItem(item)),
54      signal,
55    ),
56    Stack.flagShown(host, STACK, SUMMARY_JOBS, items, signal),
57  ])
58}
59
60// The band's rotation timer, for this load of the module; each page the band turns to gets one-line summaries while they are on, its stack items the flag check.
61const ROTATION = Band.rotationOf(showItems)
62
63// The refresh timer and the run in flight, for this load of the module; new items and the page the band shows get one-line summaries while they are on, then the stack's releases are refreshed.
64const REFRESH = Refresh.refreshLoopOf(async (host, run, signal) => {
65  await Summaries.summarizeNew(host, SUMMARY_JOBS, run.newItems, signal)
66  await Band.handShownPage(host, ROTATION, signal)
67  await Stack.refreshStack(host, STACK, SUMMARY_JOBS, signal, { redetect: true })
68})
69
70// How many items the pane last drew, for this load of the module; the render hook records it, the summaries read it.
71const PANE_WINDOW = Pane.paneWindowOf()
72
73/**
74 * Binds the Host from a hook's `$`, each engine call spelled in full.
75 *
76 * @param $ the hook's engine
77 */
78function hostOf($: EngineInterface): Host {
79  return {
80    storeGet: key => $.store.get(key),
81    storeSet: (key, value) => $.store.set(key, value),
82    state: {
83      sources: { read: () => read($, SOURCES), update: change => update($, SOURCES, change) },
84      settings: { read: () => read($, SETTINGS), update: change => update($, SETTINGS, change) },
85      items: { read: () => read($, ITEMS), update: change => update($, ITEMS, change) },
86      saved: { read: () => read($, SAVED), update: change => update($, SAVED, change) },
87      summaries: {
88        read: () => read($, SUMMARIES),
89        update: change => update($, SUMMARIES, change),
90      },
91      band: { read: () => read($, BAND), update: change => update($, BAND, change) },
92      pane: { read: () => read($, PANE), update: change => update($, PANE, change) },
93      status: { read: () => read($, STATUS), update: change => update($, STATUS, change) },
94      stack: {
95        read: () => read($, STACK_STATE),
96        update: change => update($, STACK_STATE, change),
97      },
98      read: { read: () => read($, READ), update: change => update($, READ, change) },
99      viewed: { read: () => read($, VIEWED), update: change => update($, VIEWED, change) },
100    },
101    userLanguage: async () => Store.userLanguageOf(await $.settings.read()),
102    debug: text => $.ui.log(text, { to: 'debug' }),
103    httpFetch: (url, init) => $.http.fetch(url, init),
104    modelComplete: (request, signal) => $.model.complete(request, { signal }),
105    toast: text => $.ui.toast(text),
106    clockNow: () => $.clock.now(),
107    clockEvery: (ms, fn) => $.clock.every(ms, fn),
108    clockAfter: (ms, fn) => $.clock.after(ms, fn),
109    sessionRoot: () => $.session.root(),
110    homeDir: async () => (await $.env.get('HOME')) ?? $.env.get('USERPROFILE'),
111    listDir: path => $.fs.list(path),
112    readText: path => $.fs.read(path),
113    surfaces: () => $.session.surfaces(),
114    openPane: pane => $.ui.open(pane),
115    isPaneShown: () => isPaneShown($),
116    processRun: (argv, init) => $.process.run(argv, init),
117    osVariable: () => $.env.get('OS'),
118    copy: (text, surface) => $.ui.copy({ text, surface }),
119    log: text => $.ui.log(text),
120  }
121}
122
123/**
124 * Restarts the refresh timer at the stored interval, cancelling the one before, and kicks one refresh off, not awaited.
125 *
126 * @param $ the hook's engine
127 */
128function restartRefresh($: EngineInterface): void {
129  void Refresh.restartRefresh(hostOf($), REFRESH)
130}
131
132/**
133 * Restarts the band's rotation at the stored seconds, cancelling the timer before, and summarizes the page shown, not awaited.
134 *
135 * @param $ the hook's engine
136 */
137function restartRotation($: EngineInterface): void {
138  void Band.restartRotation(hostOf($), ROTATION)
139}
140
141/**
142 * Summarizes the items the band shows now in the current language, not awaited.
143 *
144 * @param $ the hook's engine
145 */
146function resyncSummaries($: EngineInterface): void {
147  void Band.handShownPage(hostOf($), ROTATION)
148}
149
150/**
151 * Summarizes the items the pane shows now in the current language, not awaited: in the window it last drew, else `PANE_FIRST_WINDOW` when it is opening, else nothing.
152 *
153 * @param $ the hook's engine
154 * @param isOpening whether `/herald` just opened the pane
155 */
156function resyncPane($: EngineInterface, isOpening: boolean): void {
157  const size = PANE_WINDOW.size ?? (isOpening ? Pane.PANE_FIRST_WINDOW : undefined)
158
159  if (size !== undefined) {
160    void Pane.handShownPane(hostOf($), size, summarizeShown)
161  }
162}
163
164/**
165 * What the per-item action Buttons run, on the item drawn as selected; the band and the pane share them.
166 *
167 * @param $ the render hook's engine, used only when a Button is pressed
168 * @param item the selected item as drawn
169 */
170function itemActionsOf($: EngineInterface, item: Item | undefined): Actions.ItemActionHandlers {
171  const act =
172    (action: (host: Host, selected: Item, press: UiPressArgument) => Promise<boolean>) =>
173    (press: UiPressArgument) =>
174      void (item === undefined ? undefined : action(hostOf($), item, press))
175
176  return {
177    open: act((host, selected) => Actions.openItem(host, selected)),
178    summarize: act((host, selected) => Actions.summarizeItem(host, SUMMARY_JOBS, selected)),
179    save: act((host, selected) => Actions.saveItem(host, selected)),
180    copy: act((host, selected, press) => Actions.copyItem(host, selected, press.surface)),
181  }
182}
183
184/**
185 * What the band's Buttons run: page and selection moves write the band state, the actions act on the item drawn as selected.
186 *
187 * @param $ the render hook's engine, used only when a Button is pressed
188 * @param item the selected item as drawn
189 */
190function bandHandlersOf($: EngineInterface, item: Item | undefined): Band.BandHandlers {
191  const turn = (move: Band.BandMove) => () => void Band.turnBand(hostOf($), ROTATION, move)
192
193  return {
194    prev: turn('prev'),
195    next: turn('next'),
196    auto: turn('auto'),
197    up: turn('up'),
198    down: turn('down'),
199    ...itemActionsOf($, item),
200  }
201}
202
203/**
204 * One-line summaries (stack items: the flag check) for the items the pane shows after its tab, window or filter changed.
205 *
206 * @param host the engine
207 * @param items the items shown
208 */
209function summarizeShown(host: Host, items: readonly Item[]): Promise<unknown> {
210  return showItems(host, items)
211}
212
213/**
214 * What the pane's Buttons run: tab and selection moves write the pane state and summarize what comes into view, mark-as-read drops the item drawn as selected from the saved list, the releases toggle lists or hides the selected package's releases, the actions act on the item drawn as selected; typing in the stack tab's filter writes the filter.
215 *
216 * @param $ the render hook's engine, used only when a Button is pressed
217 * @param item the selected item as drawn
218 * @param size how many items the pane's window shows
219 */
220function paneHandlersOf(
221  $: EngineInterface,
222  item: Item | undefined,
223  size: number,
224): Pane.PaneHandlers {
225  const move = (to: Pane.PaneMove) => () => void Pane.movePane(hostOf($), to, size, summarizeShown)
226
227  const markRead = async (host: Host, selected: Item) => {
228    if (await Actions.markRead(host, selected)) {
229      await Pane.handShownPane(host, size, summarizeShown)
230    }
231  }
232
233  return {
234    tab: id => move({ tab: id }),
235    up: move('up'),
236    down: move('down'),
237    read: () => void (item === undefined ? undefined : markRead(hostOf($), item)),
238    releases: () => void Pane.toggleReleases(hostOf($), size, summarizeShown),
239    filter: value => void Pane.filterPane(hostOf($), value, size, summarizeShown),
240    ...itemActionsOf($, item),
241  }
242}
243
244/**
245 * Starts the stack on the next clock tick (its kept releases into state, the detection, the first refresh), so the session start never waits for the walk.
246 *
247 * @param $ the hook's engine
248 */
249function detectSoon($: EngineInterface): void {
250  try {
251    $.clock.after(0, () => void Stack.startStack(hostOf($), STACK, SUMMARY_JOBS))
252  } catch (error) {
253    $.ui.log(
254      `herald: deps: could not schedule detection: ${error instanceof Error ? error.message : String(error)}`,
255      { to: 'debug' },
256    )
257  }
258}
259
260/**
261 * Refreshes the stack's releases, detecting the stack again first when asked, not awaited; behind a refresh in flight it runs once after it.
262 *
263 * @param $ the hook's engine
264 * @param isRescan whether to detect the stack again first
265 */
266function refreshStackSoon($: EngineInterface, isRescan: boolean): void {
267  void (isRescan
268    ? Stack.rescanStack(hostOf($), STACK, SUMMARY_JOBS)
269    : Stack.refreshStack(hostOf($), STACK, SUMMARY_JOBS))
270}
271
272/**
273 * Whether the Herald pane is open, drawn and the pane the surface shows, by the engine's record of this plugin's panes; false when the engine cannot say, so the band is drawn.
274 *
275 * @param $ the render hook's engine
276 */
277async function isPaneShown($: EngineInterface): Promise<boolean> {
278  try {
279    const panes = await $.ui.panes()
280
281    return panes.some(pane => pane.id === Names.PANE_ID && pane.isShown && pane.isPlaced)
282  } catch {
283    return false
284  }
285}
286
287/**
288 * Declares `/herald` for the session; a refused registration is logged to debug, never thrown.
289 *
290 * @param $ the hook's engine
291 */
292async function registerHerald($: EngineInterface): Promise<void> {
293  try {
294    // The name stays literal: the static scan pairs it with the command.run hook.
295    await $.command.register({
296      name: 'herald',
297      description: Commands.HERALD_COMMAND.description,
298      argumentHint: Commands.HERALD_COMMAND.argumentHint,
299      immediate: true,
300    })
301  } catch (error) {
302    $.ui.log(
303      `herald: could not register /${Names.COMMAND_NAME}: ${error instanceof Error ? error.message : String(error)}`,
304      { to: 'debug' },
305    )
306  }
307}
308
309/**
310 * Registers the Herald mod's hooks.
311 *
312 * @param on the engine's registrar
313 */
314export const register: Register = on => {
315  on('session.start', async ($, e, next) => {
316    await State.hydrate(hostOf($)).catch(() => undefined)
317    restartRefresh($)
318    restartRotation($)
319    detectSoon($)
320    await registerHerald($)
321
322    return next(e)
323  })
324
325  on('command.run', { command: 'herald' }, async ($, e) => {
326    const reply = await Commands.runHerald(
327      hostOf($),
328      e.args,
329      STACK,
330      PANE_WINDOW.size ?? Pane.PANE_FIRST_WINDOW,
331    )
332
333    if (reply.restartRefresh === true) {
334      restartRefresh($)
335    }
336
337    if (reply.restartRotation === true) {
338      restartRotation($)
339    } else if (reply.resyncSummaries === true) {
340      resyncSummaries($)
341    }
342
343    if (
344      reply.summarizePane === true ||
345      reply.restartRotation === true ||
346      reply.resyncSummaries === true
347    ) {
348      resyncPane($, reply.summarizePane === true)
349    }
350
351    if (reply.rescanStack === true || reply.refreshStack === true) {
352      refreshStackSoon($, reply.rescanStack === true)
353    }
354
355    return { text: reply.text }
356  })
357
358  // The band reads state and the clock only (it notes its page size in the rotation); its Buttons write through the Host when pressed.
359  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
360    if (e.props.hasSurvey || (await isPaneShown($))) {
361      return next(e)
362    }
363
364    const sources = await read($, SOURCES)
365    const items = Band.bandItemsOf(
366      sources,
367      await read($, ITEMS),
368      Stack.shownStackItemsOf(await read($, STACK_STATE)),
369      await read($, READ),
370    )
371
372    if (items.length === 0) {
373      return next(e)
374    }
375
376    const columns = e.props.bodyColumns
377    const now = await $.clock.now()
378
379    ROTATION.pageSize = Band.bandPageSizeOf(columns, items.length)
380
381    const page = Band.bandPageOf(await read($, BAND), items, ROTATION.pageSize)
382    const { Box, Text, Button, Link } = $.ui.resolve(e)
383    const handlers = bandHandlersOf($, page.items[page.span.selected])
384
385    if (Band.isCompactBand(columns, items.length)) {
386      return Band.compactBandView(
387        { Box, Text, Button, Link },
388        Band.compactBandModelOf(page, sources, columns, now),
389        handlers,
390        await next(e),
391      )
392    }
393
394    return Band.bandView(
395      { Box, Text, Button, Link },
396      Band.bandModelOf(
397        page,
398        sources,
399        await read($, SUMMARIES),
400        await read($, SAVED),
401        columns,
402        (await read($, SETTINGS)).autoSummaries === true,
403        now,
404      ),
405      handlers,
406      await next(e),
407    )
408  })
409
410  // Opening or closing the pane draws the band again, which yields while the pane shows; a failure never holds the pane.
411  on('ui.open', { id: 'herald' }, async ($, e, next) => {
412    const opened = await next(e)
413
414    $.ui.invalidate('ui.render')
415
416    return opened
417  }).catch(($, e, next) => next(e))
418
419  on('ui.close', { id: 'herald' }, async ($, e, next) => {
420    const closed = await next(e)
421
422    $.ui.invalidate('ui.render')
423    // The page the band shows again gets its summaries now, not at the next turn.
424    resyncSummaries($)
425
426    return closed
427  }).catch(($, e, next) => next(e))
428
429  // The pane reads state and the clock only (it notes the window size in a module holder); its Buttons write through the Host when pressed.
430  // The id stays literal so validate reports it; a test mounts the pane by Names.PANE_ID to keep them equal.
431  on('ui.render', { component: 'Pane', requestId: 'herald' }, async ($, e) => {
432    const sources = await read($, SOURCES)
433    const items = await read($, ITEMS)
434    const saved = await read($, SAVED)
435    const stack = Pane.paneStackOf(await read($, STACK_STATE))
436    const viewed = await read($, VIEWED)
437    const newCounts = {
438      ...Pane.paneNewCountsOf(items, viewed),
439      [Names.ALL_TAB]: Pane.allTabNewCountOf(sources, items, viewed),
440    }
441    const settings = await read($, SETTINGS)
442    const health = {
443      ...(await read($, STATUS)),
444      refreshMinutes: settings.refreshMinutes,
445      now: await $.clock.now(),
446    }
447    const page = Pane.panePageOf(
448      await read($, PANE),
449      sources,
450      items,
451      saved,
452      Pane.paneWindowSizeOf(
453        sources,
454        e.props.bodyColumns,
455        e.props.scroll.bodyRows,
456        stack,
457        saved,
458        newCounts,
459      ),
460      stack,
461      newCounts,
462      health,
463    )
464    const table = $.ui.resolve(e)
465    const { Box, Text, Button, Link } = table
466    // Mobile and surfaces without Input get no filter field.
467    const Input = e.surface === 'mobile' || !('Input' in table) ? undefined : table.Input
468
469    PANE_WINDOW.size = page.size
470
471    return Pane.paneView(
472      { Box, Text, Button, Link, ...(Input === undefined ? {} : { Input }) },
473      Pane.paneModelOf(
474        page,
475        sources,
476        await read($, SUMMARIES),
477        saved,
478        e.props.bodyColumns,
479        settings.autoSummaries === true,
480        stack?.filter,
481        // A phone taps the Buttons, so it never needs the focus chord.
482        e.props.isFocused || e.surface === 'mobile',
483        await read($, READ),
484        health.now,
485      ),
486      paneHandlersOf($, page.items[page.selected], page.size),
487      e.surface,
488      Pane.paneFillRowsOf(e.props.placement, e.props.scroll.bodyRows),
489    )
490  })
491
492  // /clear, /resume and /branch reset $.state without a session.start.
493  on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
494    await State.hydrate(hostOf($)).catch(() => undefined)
495    await Stack.hydrateStack(hostOf($), STACK)
496    await registerHerald($)
497
498    return next(e)
499  }).catch(($, e, next) => next(e))
500}
501
hooks/actions/index.ts 20 lines
1export * from './clipboard-argvs-of.js'
2export * from './copied-toast.js'
3export * from './copy-item.js'
4export * from './copy-text-of.js'
5export * from './copy-with-tool.js'
6export * from './deps-copy-text-of.js'
7export type * from './item-action-handlers.js'
8export * from './mark-read.js'
9export * from './no-text-line.js'
10export * from './open-item.js'
11export * from './opener-argv-of.js'
12export type * from './platform.js'
13export * from './platform-of.js'
14export * from './read-item.js'
15export * from './save-item.js'
16export * from './summarize-item.js'
17export * from './title-line-of.js'
18
19export * as default from '.'
20
hooks/band/index.ts 69 lines
1export * from './action-row-view.js'
2export * from './actions-columns-of.js'
3export * from './age-ahead-ms.js'
4export * from './age-column-of.js'
5export * from './age-columns.js'
6export * from './age-gap-columns.js'
7export * from './age-of.js'
8export * from './band-after.js'
9export type * from './band-handlers.js'
10export * from './band-items-of.js'
11export type * from './band-model.js'
12export * from './band-model-of.js'
13export type * from './band-move.js'
14export type * from './band-page.js'
15export * from './band-page-of.js'
16export * from './band-page-size.js'
17export * from './band-page-size-of.js'
18export type * from './band-row.js'
19export type * from './band-span.js'
20export * from './band-span-of.js'
21export type * from './band-ui.js'
22export * from './band-view.js'
23export * from './button-columns-of.js'
24export * from './char-width-of.js'
25export type * from './compact-band-model.js'
26export * from './compact-band-model-of.js'
27export * from './compact-band-view.js'
28export * from './compact-controls-columns-of.js'
29export * from './compact-gap-columns.js'
30export * from './compact-headline-min-columns.js'
31export * from './compact-title-columns-of.js'
32export * from './display-width-of.js'
33export * from './fit-columns.js'
34export * from './group-gap-columns.js'
35export * from './hand-page.js'
36export * from './hand-shown-page.js'
37export * from './header-columns-of.js'
38export * from './headline-line-of.js'
39export type * from './headline-line.js'
40export * from './icon-columns.js'
41export * from './icon-gap-of.js'
42export * from './is-compact-band.js'
43export * from './is-release-news.js'
44export * from './last-page-start-of.js'
45export * from './lead-columns.js'
46export * from './mark-columns.js'
47export * from './newest-item-of.js'
48export * from './news-headline-of.js'
49export * from './page-start-of.js'
50export * from './range-label-of.js'
51export * from './restart-rotation.js'
52export type * from './rotation.js'
53export * from './rotation-of.js'
54export * from './selected-fill-of.js'
55export * from './selected-style-of.js'
56export * from './source-column-of.js'
57export * from './source-columns.js'
58export * from './source-gap-columns.js'
59export * from './source-head-of.js'
60export * from './source-min-columns.js'
61export * from './source-mix-of.js'
62export * from './stack-label-of.js'
63export * from './summary-indent.js'
64export * from './summary-placeholder.js'
65export * from './turn-band.js'
66export * from './wide-ranges.js'
67
68export * as default from '.'
69
hooks/commands/index.ts 77 lines
1export * from './add-dep.js'
2export * from './add-feed.js'
3export * from './add-page.js'
4export * from './add-refusal-of.js'
5export * from './apply-deps-settings.js'
6export * from './apply-settings.js'
7export * from './argument-of.js'
8export type * from './command-reply.js'
9export * from './dep-key-parts-of.js'
10export * from './deps-level-of.js'
11export * from './deps-refusal-of.js'
12export * from './deps-root-of.js'
13export * from './deps-subcommands.js'
14export * from './deps-template-placeholders.js'
15export * from './deps-text-of.js'
16export * from './deps-usage-text-of.js'
17export * from './deps-usages.js'
18export * from './digest-text-of.js'
19export * from './drop-sources.js'
20export * from './feed-failure-text-of.js'
21export * from './find-source.js'
22export * from './has-drawing-surface.js'
23export * from './http-url-of.js'
24export * from './lang-of.js'
25export * from './list-sources.js'
26export * from './list-text-of.js'
27export * from './looks-like-html.js'
28export * from './map-dep.js'
29export * from './mirror-summaries.js'
30export * from './name-key-of.js'
31export * from './herald-command.js'
32export * from './on-off-of.js'
33export * from './page-title-of.js'
34export * from './project-package-of.js'
35export * from './quoted-name-of.js'
36export * from './remove-source.js'
37export * from './rescan-deps.js'
38export * from './reset-herald.js'
39export * from './run-deps.js'
40export * from './run-herald.js'
41export * from './same-url-of.js'
42export * from './save-new-source.js'
43export * from './set-dep-ignored.js'
44export * from './set-deps-cap.js'
45export * from './set-deps-dev.js'
46export * from './set-deps-enabled.js'
47export * from './set-deps-filter.js'
48export * from './set-deps-level.js'
49export * from './set-deps-template.js'
50export * from './set-deps-toast.js'
51export * from './set-enabled.js'
52export * from './set-lang.js'
53export * from './set-refresh-interval.js'
54export * from './set-rotate.js'
55export * from './set-summaries.js'
56export * from './set-template.js'
57export * from './show-deps.js'
58export * from './show-herald.js'
59export * from './single-word-of.js'
60export * from './source-name-of.js'
61export type * from './subcommand.js'
62export * from './subcommands.js'
63export * from './template-max-chars.js'
64export * from './template-placeholders.js'
65export * from './template-text-of.js'
66export * from './typed-package-of.js'
67export * from './unmap-dep.js'
68export * from './unique-id-of.js'
69export * from './unique-name-of.js'
70export * from './url-key-of.js'
71export * from './usage-text-of.js'
72export * from './whole-number-of.js'
73export * from './without-keys.js'
74export * from './words-of.js'
75
76export * as default from '.'
77
hooks/deps/stack/index.ts 51 lines
1export * from './ecosystem-labels.js'
2export * from './flag-shown.js'
3export * from './hydrate-stack.js'
4export * from './is-at-level.js'
5export * from './is-flagged-package.js'
6export * from './is-flagged-release.js'
7export * from './is-flagged-stack-item.js'
8export * from './is-stack-item.js'
9export * from './is-suppressed.js'
10export * from './level-color-of.js'
11export * from './level-ranks.js'
12export * from './matches-stack-filter.js'
13export * from './mirror-stack.js'
14export * from './needs-lookup.js'
15export * from './package-flags-of.js'
16export * from './refresh-stack.js'
17export * from './release-of-stack-item.js'
18export * from './rescan-stack.js'
19export * from './shown-stack-items-of.js'
20export * from './stack-headline-of.js'
21export * from './stack-icon-of.js'
22export * from './stack-item-icon-of.js'
23export * from './stack-item-of-release.js'
24export * from './stack-limits.js'
25export * from './stack-line-of.js'
26export type * from './stack-loop.js'
27export * from './stack-loop-of.js'
28export * from './stack-note-of.js'
29export type * from './stack-package.js'
30export * from './stack-package-item-of.js'
31export * from './stack-package-items-of.js'
32export * from './stack-package-note-of.js'
33export * from './stack-packages-of.js'
34export * from './stack-progress-of.js'
35export type * from './stack-row.js'
36export * from './stack-row-item-of.js'
37export * from './stack-rows-of.js'
38export type * from './stack-run.js'
39export * from './stack-state-of.js'
40export * from './stack-summary-of.js'
41export * from './stack-tab-packages-of.js'
42export * from './stack-toast-text-of.js'
43export * from './stack-version-of.js'
44export * from './stack-versions-of.js'
45export * from './target-release-of.js'
46export * from './start-stack.js'
47export * from './version-change-of.js'
48export * from './with-cached-flags.js'
49
50export * as default from '.'
51
hooks/host/index.ts 5 lines
1export type * from './host.js'
2export type * from './state-cell.js'
3
4export * as default from '.'
5
hooks/names/index.ts 18 lines
1export * from './action-hotkeys.js'
2export * from './action-labels.js'
3export * from './all-tab.js'
4export * from './band-hotkeys.js'
5export * from './band-labels.js'
6export * from './band-name.js'
7export * from './command-name.js'
8export * from './pane-hotkeys.js'
9export * from './pane-id.js'
10export * from './pane-labels.js'
11export * from './pane-title.js'
12export * from './plugin-name.js'
13export * from './saved-tab.js'
14export * from './stack-tab.js'
15export * from './store-keys.js'
16
17export * as default from '.'
18
hooks/pane/index.ts 65 lines
1export * from './ago-of.js'
2export * from './all-tab-items-of.js'
3export * from './all-tab-new-count-of.js'
4export * from './filter-pane.js'
5export * from './fit-start-columns.js'
6export * from './hand-pane-items.js'
7export * from './hand-shown-pane.js'
8export * from './move-pane.js'
9export * from './pane-after.js'
10export type * from './pane-cell.js'
11export * from './pane-date-columns.js'
12export * from './pane-date-of.js'
13export * from './pane-fill-rows-of.js'
14export * from './pane-first-window.js'
15export * from './pane-fitted-tabs-of.js'
16export * from './pane-focus-hint.js'
17export type * from './pane-handlers.js'
18export type * from './pane-health.js'
19export * from './pane-heading-reserve.js'
20export type * from './pane-key.js'
21export type * from './pane-model.js'
22export * from './pane-model-of.js'
23export type * from './pane-move.js'
24export * from './pane-new-counts-of.js'
25export type * from './pane-page.js'
26export * from './pane-page-of.js'
27export * from './pane-room-of.js'
28export type * from './pane-row.js'
29export type * from './pane-shown.js'
30export * from './pane-span-of.js'
31export type * from './pane-stack.js'
32export * from './pane-stack-of.js'
33export type * from './pane-stack-page.js'
34export * from './pane-stack-rows-of.js'
35export type * from './pane-state-line.js'
36export * from './pane-state-line-of.js'
37export * from './pane-summary-lines.js'
38export type * from './pane-tab.js'
39export * from './pane-tab-columns.js'
40export * from './pane-tab-count-gap.js'
41export * from './pane-tab-count-text-of.js'
42export * from './pane-tab-keys.js'
43export * from './pane-tab-row-text-of.js'
44export * from './pane-tab-spelling-of.js'
45export type * from './pane-tab-view.js'
46export type * from './pane-ui.js'
47export * from './pane-tabs-of.js'
48export * from './pane-view.js'
49export type * from './pane-window.js'
50export * from './pane-window-of.js'
51export * from './pane-window-size-of.js'
52export * from './short-date-of.js'
53export * from './short-reason-of.js'
54export * from './shown-selected-first-of.js'
55export * from './stack-state-line-of.js'
56export * from './state-line-text-of.js'
57export * from './toggle-releases.js'
58export * from './view-items.js'
59export * from './view-shown-tab.js'
60export * from './view-tabs.js'
61export * from './wrap-columns.js'
62export * from './wrapped-lines-of.js'
63
64export * as default from '.'
65
hooks/refresh/index.ts 23 lines
1export type * from './fetched.js'
2export * from './fetch-feed.js'
3export * from './fetch-page.js'
4export * from './fit-toast.js'
5export * from './items-of-extracted.js'
6export * from './items-of-feed.js'
7export * from './map-limited.js'
8export * from './message-of.js'
9export * from './note-refreshed.js'
10export * from './refresh-all.js'
11export * from './refresh-limits.js'
12export type * from './refresh-loop.js'
13export * from './refresh-loop-of.js'
14export type * from './refresh-run.js'
15export * from './refresh-source.js'
16export * from './restart-refresh.js'
17export * from './serial-of.js'
18export type * from './source-outcome.js'
19export * from './toast-text-of.js'
20export * from './within-deadline.js'
21
22export * as default from '.'
23
hooks/state/index.ts 6 lines
1export * from './hydrate.js'
2export * from './initial-state.js'
3export type * from './herald-state.js'
4
5export * as default from '.'
6
hooks/store/index.ts 81 lines
1export * from './added-deps-of.js'
2export * from './add-read.js'
3export * from './add-saved.js'
4export * from './add-source-ids.js'
5export * from './add-viewed.js'
6export * from './capped-dep-feeds.js'
7export * from './dependency-of.js'
8export * from './dep-feed-of.js'
9export * from './dep-feeds-max.js'
10export * from './dep-feeds-of.js'
11export * from './dep-keys-of.js'
12export * from './deps-cap-bounds.js'
13export * from './deps-list-max.js'
14export * from './deps-project-of.js'
15export * from './deps-projects-max.js'
16export * from './deps-projects-of.js'
17export * from './deps-settings-of.js'
18export * from './ecosystems.js'
19export * from './forget-sources.js'
20export * from './is-record.js'
21export * from './item-of.js'
22export * from './items-by-source-of.js'
23export * from './items-of.js'
24export * from './load-dep-feeds.js'
25export * from './load-deps-project.js'
26export * from './load-items.js'
27export * from './load-page-hashes.js'
28export * from './load-refreshed-at.js'
29export * from './load-release-flags.js'
30export * from './load-read.js'
31export * from './load-saved.js'
32export * from './load-seen.js'
33export * from './load-settings.js'
34export * from './load-sources.js'
35export * from './load-stack-project.js'
36export * from './load-summaries.js'
37export * from './load-viewed.js'
38export * from './mark-seen.js'
39export * from './page-hashes-of.js'
40export * from './put-dep-override.js'
41export * from './put-release-flags.js'
42export * from './put-summary.js'
43export * from './refreshed-at-of.js'
44export * from './release-flag-entries-of.js'
45export * from './release-flags-max.js'
46export * from './remove-dep-override.js'
47export * from './remove-saved.js'
48export * from './save-dep-feeds.js'
49export * from './save-detection.js'
50export * from './save-items.js'
51export * from './save-page-hash.js'
52export * from './save-refreshed-at.js'
53export * from './save-settings.js'
54export * from './save-sources.js'
55export * from './save-stack-project.js'
56export * from './saved-of.js'
57export * from './seen-of.js'
58export * from './seed-viewed.js'
59export * from './seen-per-source.js'
60export * from './settings-of.js'
61export * from './source-of.js'
62export * from './sources-of.js'
63export * from './stack-item-of.js'
64export * from './stack-project-of.js'
65export * from './stack-projects-max.js'
66export * from './stack-release-of.js'
67export * from './summaries-for.js'
68export * from './summary-cache-max.js'
69export * from './summary-entries-of.js'
70export * from './summary-key-of.js'
71export * from './summary-lang-of.js'
72export * from './summary-of.js'
73export * from './text-of.js'
74export * from './update-deps-project.js'
75export * from './update-settings.js'
76export * from './user-language-of.js'
77export * from './with-release-flags.js'
78export * from './with-summary.js'
79
80export * as default from '.'
81
hooks/summaries/index.ts 25 lines
1export * from './current-summary-lang.js'
2export * from './ensure-visible-summaries.js'
3export * from './excerpt-of.js'
4export * from './is-auto-summarizing.js'
5export * from './is-meta-reply.js'
6export * from './is-muted.js'
7export type * from './limiter.js'
8export * from './limiter-of.js'
9export * from './long-summary-of.js'
10export type * from './rejection.js'
11export * from './run-limited.js'
12export * from './short-summary-of.js'
13export * from './summarize.js'
14export * from './summarize-long.js'
15export * from './summarize-new.js'
16export type * from './summary-jobs.js'
17export * from './summary-jobs-of.js'
18export * from './summary-limits.js'
19export * from './summary-prompt-version.js'
20export type * from './summary-request.js'
21export * from './summary-request-of.js'
22export * from './summary-text-of.js'
23
24export * as default from '.'
25