@wangjunjian/dsh-github-trending 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,352 @@
1
+ /**
2
+ * README-backed repository overviews generated through the harness LLM service.
3
+ *
4
+ * When enabled, every successful cache refresh enqueues the entry with the
5
+ * {@link OverviewGenerator}, which produces a short localized overview per
6
+ * repository, in rank order, current panel window first. Generation runs in
7
+ * the background and never blocks or fails the refresh.
8
+ *
9
+ * @module @wangjunjian/dsh-github-trending/summaries
10
+ */
11
+ import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm';
12
+ import { USER_AGENT } from './tool.js';
13
+ /** README filenames tried in order under the repository's default branch. */
14
+ const README_CANDIDATES = ['README.md', 'readme.md', 'README.rst', 'README'];
15
+ /** README input is truncated to this many characters before summarization. */
16
+ const README_MAX_CHARS = 8000;
17
+ /**
18
+ * Fetch a repository's README from its default branch.
19
+ *
20
+ * @param fullName - `owner/name` repository path.
21
+ * @param signal - optional cancellation signal.
22
+ * @returns the truncated README text, or `undefined` when unavailable.
23
+ */
24
+ export async function fetchReadmeText(fullName, signal) {
25
+ for (const candidate of README_CANDIDATES) {
26
+ let response;
27
+ try {
28
+ response = await fetch(`https://raw.githubusercontent.com/${fullName}/HEAD/${candidate}`, {
29
+ headers: { 'User-Agent': USER_AGENT },
30
+ signal,
31
+ });
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ if (response.status === 404)
37
+ continue;
38
+ if (!response.ok)
39
+ return undefined;
40
+ const text = await response.text();
41
+ return text.slice(0, README_MAX_CHARS);
42
+ }
43
+ return undefined;
44
+ }
45
+ /** Abort signal that fires after `timeoutMs`, with its timer handle. */
46
+ function deadline(timeoutMs) {
47
+ const controller = new AbortController();
48
+ const timer = setTimeout(() => {
49
+ controller.abort();
50
+ }, timeoutMs);
51
+ return { signal: controller.signal, cancel: () => clearTimeout(timer) };
52
+ }
53
+ /** Display names for the languages LLM generations can be written in. */
54
+ export const LANGUAGE_NAMES = {
55
+ zh: 'Chinese (中文)',
56
+ en: 'English',
57
+ };
58
+ /**
59
+ * Stream one auxiliary LLM call to completion and return its text.
60
+ *
61
+ * A `max-tokens` finish keeps the text already received (the assembler drops
62
+ * only tool-call blocks) — slightly truncated prose beats no output for these
63
+ * best-effort generations. Other non-`stop` finishes throw, as does empty
64
+ * text; error messages name the call site and the provider/model route so a
65
+ * misbehaving route is diagnosable from logs.
66
+ *
67
+ * @param ctx - host context providing the `llm` service.
68
+ * @param route - provider/model route for the call.
69
+ * @param request - messages/system plus budget and a label for error messages.
70
+ * @returns the generated text.
71
+ */
72
+ export async function streamGeneratedText(ctx, route, request) {
73
+ const call = deadline(request.timeoutMs);
74
+ try {
75
+ const assembler = new BlockAssembler();
76
+ for await (const chunk of ctx.llm.stream({
77
+ provider: route.provider,
78
+ model: route.model,
79
+ messages: request.messages,
80
+ system: request.system,
81
+ maxTokens: request.maxTokens,
82
+ signal: call.signal,
83
+ })) {
84
+ assembler.push(chunk);
85
+ }
86
+ const finish = assembler.finish;
87
+ if (finish.kind !== 'stop' && finish.kind !== 'max-tokens') {
88
+ const detail = 'failure' in finish ? finish.failure.message : finish.kind;
89
+ throw new Error(`${request.label} via ${route.provider}/${route.model} finished with ${finish.kind}: ${detail}`);
90
+ }
91
+ const text = assembler
92
+ .blocks()
93
+ .filter((block) => block.type === 'text')
94
+ .map((block) => block.text)
95
+ .join('')
96
+ .trim();
97
+ if (text === '') {
98
+ throw new Error(`${request.label} via ${route.provider}/${route.model} returned empty text (finish: ${finish.kind})`);
99
+ }
100
+ return text;
101
+ }
102
+ finally {
103
+ call.cancel();
104
+ }
105
+ }
106
+ /**
107
+ * Generate a localized one-paragraph overview for a repository via the harness
108
+ * LLM service.
109
+ *
110
+ * @param ctx - host context providing the `llm` service.
111
+ * @param route - provider/model route for the call.
112
+ * @param repo - the trending repository being summarized.
113
+ * @param readme - truncated README text.
114
+ * @param options - resolved overview options.
115
+ * @returns the overview text.
116
+ */
117
+ export async function summarizeRepository(ctx, route, repo, readme, options) {
118
+ const system = `You write concise overviews of GitHub repositories for a trending list. Write in ${LANGUAGE_NAMES[options.language]}. Reply with a single short paragraph (at most 80 words) explaining what the project is, what problem it solves, and why it might be gaining attention. No markdown, no preamble, no headings.`;
119
+ const messages = [
120
+ createUserMessage({
121
+ content: [
122
+ {
123
+ type: 'text',
124
+ text: `Repository: ${repo.fullName}\nDescription: ${repo.description ?? '(none)'}\n\nREADME (truncated):\n${readme}`,
125
+ },
126
+ ],
127
+ source: { kind: 'plugin', plugin: 'github-trending' },
128
+ }),
129
+ ];
130
+ return streamGeneratedText(ctx, route, {
131
+ messages,
132
+ system,
133
+ maxTokens: options.maxTokens,
134
+ timeoutMs: options.timeoutMs,
135
+ label: `overview generation for ${repo.fullName}`,
136
+ });
137
+ }
138
+ /**
139
+ * Enrich the top repositories of a refreshed cache entry with overviews.
140
+ *
141
+ * Runs sequentially to avoid bursting the LLM provider; a failure for one
142
+ * repository skips it and never fails the whole pass.
143
+ *
144
+ * @param ctx - host context providing the `llm` service.
145
+ * @param route - provider/model route for the calls.
146
+ * @param entry - the freshly refreshed cache entry (mutated in place).
147
+ * @param options - resolved overview options.
148
+ * @param hooks - optional per-repository progress hooks.
149
+ */
150
+ export async function enrichEntryWithOverviews(ctx, route, entry, options, hooks) {
151
+ for (const repo of entry.repositories.slice(0, options.maxRepos)) {
152
+ if (hooks?.isStale?.() === true)
153
+ return;
154
+ hooks?.onRepoStart?.(repo);
155
+ let outcome = 'skipped';
156
+ let skipReason;
157
+ try {
158
+ const call = deadline(options.timeoutMs);
159
+ let readme;
160
+ try {
161
+ readme = await fetchReadmeText(repo.fullName, call.signal);
162
+ }
163
+ finally {
164
+ call.cancel();
165
+ }
166
+ if (readme === undefined) {
167
+ skipReason = 'no README found on the default branch';
168
+ }
169
+ else {
170
+ repo.overview = await summarizeRepository(ctx, route, repo, readme, options);
171
+ outcome = 'generated';
172
+ }
173
+ }
174
+ catch (error) {
175
+ // Skip this repository; the next refresh cycle will try again. Log the
176
+ // reason so a misbehaving provider/model route is diagnosable instead of
177
+ // silently producing no overviews.
178
+ skipReason = error instanceof Error ? error.message : String(error);
179
+ ctx.logger.warn('github-trending: overview generation skipped for %s: %s', repo.fullName, skipReason);
180
+ }
181
+ finally {
182
+ hooks?.onRepoDone?.(repo, outcome, skipReason);
183
+ }
184
+ }
185
+ }
186
+ /** Cache key format shared with the cache module: `${language}:${since}`. */
187
+ function pendingKey(language, since) {
188
+ return `${language ?? ''}:${since}`;
189
+ }
190
+ /**
191
+ * Sequential overview scheduler.
192
+ *
193
+ * Refreshed cache entries are queued here instead of being enriched inline, so
194
+ * only one LLM call is in flight at a time and the window the user is looking
195
+ * at (reported via {@link setActiveSince} from the web route) is always
196
+ * processed first, from its first repository onward. The model route is
197
+ * resolved lazily at the start of every pass, so the current default-model
198
+ * selection always applies even if it changed after this plugin loaded.
199
+ * A re-request for the same
200
+ * key supersedes the queued entry and abandons any in-flight pass over the old
201
+ * entry object (refresh supersedes refresh), so the generator never spends
202
+ * minutes mutating an entry the cache has already replaced.
203
+ *
204
+ * Repositories whose overview could not be generated are remembered per entry
205
+ * ({@link failureReason}) so the web route can stop flagging them as pending
206
+ * instead of showing a permanent "generating" state, and can surface the
207
+ * reason; the next refresh creates a fresh entry object and retries them.
208
+ *
209
+ * {@link waitForChange} lets the web route long-poll: it resolves on every
210
+ * repo start/finish, so the panel learns about each overview the moment it
211
+ * lands.
212
+ */
213
+ export class OverviewGenerator {
214
+ ctx;
215
+ resolveRoute;
216
+ options;
217
+ pending = new Map();
218
+ versions = new Map();
219
+ failures = new WeakMap();
220
+ listeners = new Set();
221
+ activeSince = 'daily';
222
+ draining = false;
223
+ current;
224
+ constructor(ctx, resolveRoute, options) {
225
+ this.ctx = ctx;
226
+ this.resolveRoute = resolveRoute;
227
+ this.options = options;
228
+ }
229
+ /** Tell the generator which time window the panel is currently showing. */
230
+ setActiveSince(since) {
231
+ this.activeSince = since;
232
+ }
233
+ /**
234
+ * Queue a freshly refreshed entry for overview generation. Fire-and-forget:
235
+ * generation runs in the background and never throws back to the caller.
236
+ *
237
+ * @param entry - the refreshed cache entry.
238
+ * @param language - optional language filter of the entry.
239
+ * @param since - time window of the entry.
240
+ */
241
+ request(entry, language, since) {
242
+ const key = pendingKey(language, since);
243
+ this.versions.set(key, (this.versions.get(key) ?? 0) + 1);
244
+ this.pending.set(key, entry);
245
+ void this.drain();
246
+ }
247
+ /** Number of queued windows still waiting for generation (for diagnostics). */
248
+ get pendingCount() {
249
+ return this.pending.size;
250
+ }
251
+ /** Full name of the repository whose overview is being generated right now. */
252
+ get currentFullName() {
253
+ return this.current;
254
+ }
255
+ /**
256
+ * Why a previous pass over this entry failed to produce an overview for the
257
+ * repository, or `undefined` when it has not failed. Failures are tracked
258
+ * per entry object, so a refresh (which creates a new entry) resets them and
259
+ * the repository is retried.
260
+ */
261
+ failureReason(entry, fullName) {
262
+ return this.failures.get(entry)?.get(fullName);
263
+ }
264
+ /**
265
+ * Resolve on the next generation state change (a repository starting or
266
+ * finishing), or after `timeoutMs` — whichever comes first. Multiple callers
267
+ * may wait concurrently.
268
+ *
269
+ * @param timeoutMs - maximum time to wait.
270
+ */
271
+ waitForChange(timeoutMs) {
272
+ return new Promise((resolve) => {
273
+ const onChange = () => {
274
+ clearTimeout(timer);
275
+ resolve();
276
+ };
277
+ const timer = setTimeout(() => {
278
+ this.listeners.delete(onChange);
279
+ resolve();
280
+ }, timeoutMs);
281
+ this.listeners.add(onChange);
282
+ });
283
+ }
284
+ /** Wake every pending {@link waitForChange} caller. */
285
+ notifyChange() {
286
+ const listeners = [...this.listeners];
287
+ this.listeners.clear();
288
+ for (const listener of listeners)
289
+ listener();
290
+ }
291
+ recordFailure(entry, fullName, reason) {
292
+ let failed = this.failures.get(entry);
293
+ if (failed === undefined) {
294
+ failed = new Map();
295
+ this.failures.set(entry, failed);
296
+ }
297
+ failed.set(fullName, reason);
298
+ }
299
+ pickNext() {
300
+ const keys = [...this.pending.keys()];
301
+ const activeKey = keys.find((key) => key.endsWith(`:${this.activeSince}`));
302
+ const key = activeKey ?? keys[0];
303
+ if (key === undefined)
304
+ return undefined;
305
+ const entry = this.pending.get(key);
306
+ this.pending.delete(key);
307
+ if (entry === undefined)
308
+ return undefined;
309
+ return { key, entry, version: this.versions.get(key) ?? 0 };
310
+ }
311
+ async drain() {
312
+ if (this.draining)
313
+ return;
314
+ this.draining = true;
315
+ try {
316
+ for (;;) {
317
+ const next = this.pickNext();
318
+ if (next === undefined)
319
+ return;
320
+ const { key, entry, version } = next;
321
+ try {
322
+ // Resolve the model route per pass so a default-model change (or a
323
+ // settings service that mounted after this plugin loaded) takes
324
+ // effect without a host restart.
325
+ const route = this.resolveRoute();
326
+ await enrichEntryWithOverviews(this.ctx, route, entry, this.options, {
327
+ isStale: () => this.versions.get(key) !== version,
328
+ onRepoStart: (repo) => {
329
+ this.current = repo.fullName;
330
+ this.notifyChange();
331
+ },
332
+ onRepoDone: (repo, outcome, skipReason) => {
333
+ this.current = undefined;
334
+ if (outcome === 'skipped')
335
+ this.recordFailure(entry, repo.fullName, skipReason ?? 'unknown');
336
+ this.notifyChange();
337
+ },
338
+ });
339
+ }
340
+ catch {
341
+ // A pass-level failure (e.g. the plugin context was disposed while
342
+ // generation was in flight) must never reject the fire-and-forget
343
+ // drain; the next refresh will re-queue the window.
344
+ }
345
+ }
346
+ }
347
+ finally {
348
+ this.draining = false;
349
+ }
350
+ }
351
+ }
352
+ //# sourceMappingURL=summaries.js.map
@@ -16,6 +16,12 @@ export interface TrendingCacheOptions {
16
16
  intervalMs: number;
17
17
  /** Request timeout in milliseconds. */
18
18
  timeoutMs: number;
19
+ /**
20
+ * Optional hook invoked after every successful refresh (including scheduled
21
+ * ones). Called synchronously with the fresh entry; synchronous throws are
22
+ * swallowed so a hook failure can never break caching.
23
+ */
24
+ onRefreshed?: (entry: TrendingCacheEntry, language: string | undefined, since: string) => void;
19
25
  }
20
26
  /**
21
27
  * Simple in-memory cache with periodic refresh.
@@ -37,6 +43,14 @@ export declare class TrendingCache {
37
43
  * @returns the cached entry, or undefined when cold.
38
44
  */
39
45
  get(language: string | undefined, since: string): TrendingCacheEntry | undefined;
46
+ /**
47
+ * Find a repository by full name across all cached entries (any language,
48
+ * any window). Used to attach trending metadata to on-demand generations.
49
+ *
50
+ * @param fullName - `owner/name` repository path.
51
+ * @returns the repository, or undefined when not currently cached.
52
+ */
53
+ findRepository(fullName: string): TrendingRepository | undefined;
40
54
  /**
41
55
  * Fetch fresh data and store it. Reuses the configured timeout.
42
56
  *
@@ -46,6 +60,8 @@ export declare class TrendingCache {
46
60
  * @returns the freshly cached entry.
47
61
  */
48
62
  refresh(fetcher: (signal?: AbortSignal) => Promise<TrendingRepository[]>, language: string | undefined, since: string): Promise<TrendingCacheEntry>;
63
+ /** Invoke the optional onRefreshed hook, swallowing synchronous failures. */
64
+ private notifyRefreshed;
49
65
  /**
50
66
  * Fetch with a per-attempt timeout, retrying transient failures.
51
67
  *
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @module @wangjunjian/dsh-github-trending/client/locales
5
5
  */
6
- export type GithubTrendingKey = 'action.label' | 'action.close' | 'panel.title' | 'panel.daily' | 'panel.weekly' | 'panel.monthly' | 'panel.refresh' | 'panel.loading' | 'panel.empty' | 'panel.error' | 'panel.cachedAt' | 'panel.autoRefresh' | 'panel.collapse' | 'repo.stars' | 'repo.forks' | 'repo.today';
6
+ export type GithubTrendingKey = 'action.label' | 'action.close' | 'panel.title' | 'panel.daily' | 'panel.weekly' | 'panel.monthly' | 'panel.refresh' | 'panel.loading' | 'panel.empty' | 'panel.error' | 'panel.cachedAt' | 'panel.autoRefresh' | 'panel.collapse' | 'panel.overviewPending' | 'intro.hint' | 'intro.loading' | 'intro.error' | 'intro.retry' | 'intro.close' | 'repo.stars' | 'repo.forks' | 'repo.today';
7
7
  export declare const en: Record<GithubTrendingKey, string>;
8
8
  export declare const zh: Record<GithubTrendingKey, string>;
9
9
  //# sourceMappingURL=locales.d.ts.map
@@ -24,6 +24,28 @@ export interface Config {
24
24
  maxResults?: number;
25
25
  /** Background refresh interval (ms) for the UI cache. Defaults to 14400000 (4 hours). */
26
26
  refreshIntervalMs?: number;
27
+ /** Whether to generate LLM README overviews after each refresh. Defaults to false. */
28
+ overviewsEnabled?: boolean;
29
+ /** Language the overviews are written in: "zh" or "en". Defaults to "zh". */
30
+ overviewsLanguage?: string;
31
+ /** Only the top N repositories per window receive an overview. Defaults to 10. */
32
+ overviewsMaxRepos?: number;
33
+ /** Optional provider route override for overview calls (requires overviewsModel). */
34
+ overviewsProvider?: string;
35
+ /** Optional model id override for overview calls (requires overviewsProvider). */
36
+ overviewsModel?: string;
37
+ /**
38
+ * Output token cap per overview call. Defaults to 2048 — reasoning models
39
+ * spend tokens on thinking before emitting text, so a small cap starves the
40
+ * actual paragraph (observed as empty text with a max-tokens finish).
41
+ */
42
+ overviewsMaxTokens?: number;
43
+ /** Per-call timeout budget (ms) for one README fetch or LLM call. Defaults to 60000. */
44
+ overviewsTimeoutMs?: number;
45
+ /** Output token cap per introduction call. Defaults to 4096. */
46
+ introsMaxTokens?: number;
47
+ /** Timeout budget (ms) for one introduction generation. Defaults to 120000. */
48
+ introsTimeoutMs?: number;
27
49
  }
28
50
  /** Cordis plugin name used by loader diagnostics. */
29
51
  export declare const name = "github-trending";
@@ -0,0 +1,65 @@
1
+ /**
2
+ * On-demand, README-backed project introductions generated through the harness
3
+ * LLM service.
4
+ *
5
+ * Unlike the automatic per-refresh overviews, a full introduction is generated
6
+ * only when the user clicks a repository's overview area in the panel
7
+ * (`GET /github-trending?intro=owner/name`), then cached in memory for the
8
+ * lifetime of the host process. Concurrent requests for the same repository
9
+ * share one in-flight generation.
10
+ *
11
+ * @module @wangjunjian/dsh-github-trending/intros
12
+ */
13
+ import type { Context } from '@deepseek-ai/cordis';
14
+ import type { TrendingRepository } from './parser.js';
15
+ import { type OverviewRoute } from './summaries.js';
16
+ /** Resolved introduction-generation options from the plugin config. */
17
+ export interface IntrosOptions {
18
+ /** Language the introduction is written in. */
19
+ language: 'zh' | 'en';
20
+ /** Output token cap per LLM call (reasoning models need the headroom). */
21
+ maxTokens: number;
22
+ /** Timeout budget (ms) for the whole generation. */
23
+ timeoutMs: number;
24
+ }
25
+ /**
26
+ * Generate a structured Markdown introduction for a repository via the harness
27
+ * LLM service.
28
+ *
29
+ * @param ctx - host context providing the `llm` service.
30
+ * @param route - provider/model route for the call.
31
+ * @param repo - trending metadata when the repository is in the cache.
32
+ * @param fullName - `owner/name` repository path.
33
+ * @param readme - truncated README text.
34
+ * @param options - resolved introduction options.
35
+ * @returns the Markdown introduction.
36
+ */
37
+ export declare function generateIntroduction(ctx: Context, route: OverviewRoute, repo: TrendingRepository | undefined, fullName: string, readme: string, options: IntrosOptions): Promise<string>;
38
+ /**
39
+ * On-demand introduction generator with in-memory caching and in-flight
40
+ * deduplication.
41
+ *
42
+ * Results are cached per `language:fullName` for the lifetime of the host
43
+ * process — introductions are prose about a repository's README, which rarely
44
+ * changes within a session. Failures are never cached, so the next click
45
+ * retries.
46
+ */
47
+ export declare class IntroductionGenerator {
48
+ private readonly ctx;
49
+ private readonly resolveRoute;
50
+ private readonly options;
51
+ private readonly cache;
52
+ private readonly inflight;
53
+ constructor(ctx: Context, resolveRoute: () => OverviewRoute, options: IntrosOptions);
54
+ /**
55
+ * Get the introduction for a repository, generating it on first request.
56
+ *
57
+ * @param fullName - `owner/name` repository path.
58
+ * @param repo - trending metadata when available in the cache.
59
+ * @returns the Markdown introduction.
60
+ * @throws when the README is unavailable or the LLM call fails.
61
+ */
62
+ get(fullName: string, repo: TrendingRepository | undefined): Promise<string>;
63
+ private generate;
64
+ }
65
+ //# sourceMappingURL=intros.d.ts.map
@@ -30,6 +30,8 @@ export interface TrendingRepository {
30
30
  forks: number;
31
31
  /** Stars gained today (or this week/month), if present. */
32
32
  starsToday: number;
33
+ /** LLM-generated localized overview; filled by the summaries module, never by the parser. */
34
+ overview?: string;
33
35
  }
34
36
  /**
35
37
  * Parse the GitHub Trending HTML and return the extracted repositories.
@@ -0,0 +1,174 @@
1
+ /**
2
+ * README-backed repository overviews generated through the harness LLM service.
3
+ *
4
+ * When enabled, every successful cache refresh enqueues the entry with the
5
+ * {@link OverviewGenerator}, which produces a short localized overview per
6
+ * repository, in rank order, current panel window first. Generation runs in
7
+ * the background and never blocks or fails the refresh.
8
+ *
9
+ * @module @wangjunjian/dsh-github-trending/summaries
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ import { type GenerateOptions } from '@deepseek-ai/dsh-llm';
13
+ import type { TrendingCacheEntry } from './cache.js';
14
+ import type { TrendingRepository } from './parser.js';
15
+ /** Model route for the auxiliary overview calls. */
16
+ export interface OverviewRoute {
17
+ /** Registered provider route. */
18
+ provider: string;
19
+ /** Provider-owned model id. */
20
+ model: string;
21
+ }
22
+ /** Resolved overview-generation options from the plugin config. */
23
+ export interface OverviewsOptions {
24
+ /** Language the overview is written in. */
25
+ language: 'zh' | 'en';
26
+ /** Only the top N repositories of each window receive an overview. */
27
+ maxRepos: number;
28
+ /** Output token cap per LLM call. */
29
+ maxTokens: number;
30
+ /** Per-call timeout budget (ms) for one README fetch or LLM call. */
31
+ timeoutMs: number;
32
+ }
33
+ /**
34
+ * Fetch a repository's README from its default branch.
35
+ *
36
+ * @param fullName - `owner/name` repository path.
37
+ * @param signal - optional cancellation signal.
38
+ * @returns the truncated README text, or `undefined` when unavailable.
39
+ */
40
+ export declare function fetchReadmeText(fullName: string, signal?: AbortSignal): Promise<string | undefined>;
41
+ /** Display names for the languages LLM generations can be written in. */
42
+ export declare const LANGUAGE_NAMES: Record<OverviewsOptions['language'], string>;
43
+ /**
44
+ * Stream one auxiliary LLM call to completion and return its text.
45
+ *
46
+ * A `max-tokens` finish keeps the text already received (the assembler drops
47
+ * only tool-call blocks) — slightly truncated prose beats no output for these
48
+ * best-effort generations. Other non-`stop` finishes throw, as does empty
49
+ * text; error messages name the call site and the provider/model route so a
50
+ * misbehaving route is diagnosable from logs.
51
+ *
52
+ * @param ctx - host context providing the `llm` service.
53
+ * @param route - provider/model route for the call.
54
+ * @param request - messages/system plus budget and a label for error messages.
55
+ * @returns the generated text.
56
+ */
57
+ export declare function streamGeneratedText(ctx: Context, route: OverviewRoute, request: {
58
+ messages: GenerateOptions['messages'];
59
+ system: string;
60
+ maxTokens: number;
61
+ timeoutMs: number;
62
+ label: string;
63
+ }): Promise<string>;
64
+ /**
65
+ * Generate a localized one-paragraph overview for a repository via the harness
66
+ * LLM service.
67
+ *
68
+ * @param ctx - host context providing the `llm` service.
69
+ * @param route - provider/model route for the call.
70
+ * @param repo - the trending repository being summarized.
71
+ * @param readme - truncated README text.
72
+ * @param options - resolved overview options.
73
+ * @returns the overview text.
74
+ */
75
+ export declare function summarizeRepository(ctx: Context, route: OverviewRoute, repo: TrendingRepository, readme: string, options: OverviewsOptions): Promise<string>;
76
+ /** Outcome of a single repository's overview attempt. */
77
+ export type EnrichOutcome = 'generated' | 'skipped';
78
+ /** Progress hooks for {@link enrichEntryWithOverviews}. */
79
+ export interface EnrichHooks {
80
+ /** Called right before a repository's README fetch + LLM call starts. */
81
+ onRepoStart?: (repo: TrendingRepository) => void;
82
+ /**
83
+ * Called after a repository finishes, with its outcome and — for `skipped`
84
+ * outcomes — a short human-readable reason.
85
+ */
86
+ onRepoDone?: (repo: TrendingRepository, outcome: EnrichOutcome, skipReason?: string) => void;
87
+ /** Checked before each repository; returning true abandons the rest of the pass. */
88
+ isStale?: () => boolean;
89
+ }
90
+ /**
91
+ * Enrich the top repositories of a refreshed cache entry with overviews.
92
+ *
93
+ * Runs sequentially to avoid bursting the LLM provider; a failure for one
94
+ * repository skips it and never fails the whole pass.
95
+ *
96
+ * @param ctx - host context providing the `llm` service.
97
+ * @param route - provider/model route for the calls.
98
+ * @param entry - the freshly refreshed cache entry (mutated in place).
99
+ * @param options - resolved overview options.
100
+ * @param hooks - optional per-repository progress hooks.
101
+ */
102
+ export declare function enrichEntryWithOverviews(ctx: Context, route: OverviewRoute, entry: TrendingCacheEntry, options: OverviewsOptions, hooks?: EnrichHooks): Promise<void>;
103
+ /**
104
+ * Sequential overview scheduler.
105
+ *
106
+ * Refreshed cache entries are queued here instead of being enriched inline, so
107
+ * only one LLM call is in flight at a time and the window the user is looking
108
+ * at (reported via {@link setActiveSince} from the web route) is always
109
+ * processed first, from its first repository onward. The model route is
110
+ * resolved lazily at the start of every pass, so the current default-model
111
+ * selection always applies even if it changed after this plugin loaded.
112
+ * A re-request for the same
113
+ * key supersedes the queued entry and abandons any in-flight pass over the old
114
+ * entry object (refresh supersedes refresh), so the generator never spends
115
+ * minutes mutating an entry the cache has already replaced.
116
+ *
117
+ * Repositories whose overview could not be generated are remembered per entry
118
+ * ({@link failureReason}) so the web route can stop flagging them as pending
119
+ * instead of showing a permanent "generating" state, and can surface the
120
+ * reason; the next refresh creates a fresh entry object and retries them.
121
+ *
122
+ * {@link waitForChange} lets the web route long-poll: it resolves on every
123
+ * repo start/finish, so the panel learns about each overview the moment it
124
+ * lands.
125
+ */
126
+ export declare class OverviewGenerator {
127
+ private readonly ctx;
128
+ private readonly resolveRoute;
129
+ private readonly options;
130
+ private readonly pending;
131
+ private readonly versions;
132
+ private readonly failures;
133
+ private readonly listeners;
134
+ private activeSince;
135
+ private draining;
136
+ private current;
137
+ constructor(ctx: Context, resolveRoute: () => OverviewRoute, options: OverviewsOptions);
138
+ /** Tell the generator which time window the panel is currently showing. */
139
+ setActiveSince(since: string): void;
140
+ /**
141
+ * Queue a freshly refreshed entry for overview generation. Fire-and-forget:
142
+ * generation runs in the background and never throws back to the caller.
143
+ *
144
+ * @param entry - the refreshed cache entry.
145
+ * @param language - optional language filter of the entry.
146
+ * @param since - time window of the entry.
147
+ */
148
+ request(entry: TrendingCacheEntry, language: string | undefined, since: string): void;
149
+ /** Number of queued windows still waiting for generation (for diagnostics). */
150
+ get pendingCount(): number;
151
+ /** Full name of the repository whose overview is being generated right now. */
152
+ get currentFullName(): string | undefined;
153
+ /**
154
+ * Why a previous pass over this entry failed to produce an overview for the
155
+ * repository, or `undefined` when it has not failed. Failures are tracked
156
+ * per entry object, so a refresh (which creates a new entry) resets them and
157
+ * the repository is retried.
158
+ */
159
+ failureReason(entry: TrendingCacheEntry, fullName: string): string | undefined;
160
+ /**
161
+ * Resolve on the next generation state change (a repository starting or
162
+ * finishing), or after `timeoutMs` — whichever comes first. Multiple callers
163
+ * may wait concurrently.
164
+ *
165
+ * @param timeoutMs - maximum time to wait.
166
+ */
167
+ waitForChange(timeoutMs: number): Promise<void>;
168
+ /** Wake every pending {@link waitForChange} caller. */
169
+ private notifyChange;
170
+ private recordFailure;
171
+ private pickNext;
172
+ private drain;
173
+ }
174
+ //# sourceMappingURL=summaries.d.ts.map