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

News and releases for your stack, right above the prompt.
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.

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.
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.
| Key | What it does |
|---|---|
p, n | Shows the previous or next page. |
a | Pauses or resumes the rotation. |
k, j | Selects the item above or below. |
o | Opens the item in your browser. |
s | Writes a 3 to 5 line Haiku summary under the item in the transcript. |
v | Saves the item for later. |
c | Copies 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.
/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.

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

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.
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.
Herald asks Claude Code only for what its features need:
package.json and uv.lock. Files stack detection reads has the full list./herald deps off.For the exact calls, hosts, files and accepted risks, see Security and privacy details.
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.| Command | What it does | ||
|---|---|---|---|
/herald | Opens the pane. | ||
/herald deps | Shows 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 off | Stops release toasts. | ||
/herald deps off | Stops following this project's dependencies and leaves the other sources alone. | ||
/herald disable Hacker News | Turns 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 on | Writes 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.
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.
/herald deps subcommands.claude -p, VS Code and cloud sessions, /herald answers with the latest items as text and /herald deps lists your stack as text.MIT, see LICENSE.
hooks/register.tsx 501 lines1import 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}
501hooks/actions/index.ts 20 lines1export * 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 '.'
20hooks/band/index.ts 69 lines1export * 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 '.'
69hooks/commands/index.ts 77 lines1export * 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 '.'
77hooks/deps/stack/index.ts 51 lines1export * 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 '.'
51hooks/host/index.ts 5 lines1export type * from './host.js'
2export type * from './state-cell.js'
3
4export * as default from '.'
5hooks/names/index.ts 18 lines1export * 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 '.'
18hooks/pane/index.ts 65 lines1export * 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 '.'
65hooks/refresh/index.ts 23 lines1export 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 '.'
23hooks/state/index.ts 6 lines1export * from './hydrate.js'
2export * from './initial-state.js'
3export type * from './herald-state.js'
4
5export * as default from '.'
6hooks/store/index.ts 81 lines1export * 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 '.'
81hooks/summaries/index.ts 25 lines1export * 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