yt-briefing 1.3.1 → 1.3.3
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.
- package/dist/lib/paths.js +1 -0
- package/dist/lib/video-dates.js +45 -0
- package/dist/lib/yt-lib.js +18 -0
- package/dist/yt-channel-pending.js +9 -2
- package/dist/yt-rating.js +10 -2
- package/dist/yt-sweep.js +44 -26
- package/package.json +1 -1
- package/plugin/hooks/register.tsx +16 -14
package/dist/lib/paths.js
CHANGED
|
@@ -54,6 +54,7 @@ export const REST_FILE = join(CACHE_DIR, 'queue-rest.json');
|
|
|
54
54
|
export const PENDING_FILE = join(CACHE_DIR, 'pending.json');
|
|
55
55
|
export const PREFETCH_FILE = join(CACHE_DIR, 'prefetch.json');
|
|
56
56
|
export const LOG_FILE = join(CACHE_DIR, 'sweep.log');
|
|
57
|
+
export const VIDEO_DATES_FILE = join(CACHE_DIR, 'video-dates.json'); // videoId → publishedAt, for the monotonic pointer
|
|
57
58
|
// /yt-search (ad-hoc topic search → lazy triage → compare). All throwaway, under .cache/.
|
|
58
59
|
export const SEARCH_QUEUE_FILE = join(CACHE_DIR, 'search-queue.json'); // ranked candidates + cursor
|
|
59
60
|
export const SEARCH_PENDING_FILE = join(CACHE_DIR, 'search-pending.json'); // current candidate awaiting keep/skip
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publish dates of every video the sweep has listed, so a state pointer bump can refuse to
|
|
3
|
+
* move back in time (see pointerWouldRegress). Kept across --reset: the pointer outlives the
|
|
4
|
+
* queue. Best-effort cache — a missing or corrupt file only turns the guard off.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { dirname } from 'node:path';
|
|
8
|
+
import { VIDEO_DATES_FILE } from "./paths.js";
|
|
9
|
+
/** Entries older than this are dropped on write; the channel listing looks back 45 days. */
|
|
10
|
+
const KEEP_DAYS = 120;
|
|
11
|
+
export function loadVideoDates() {
|
|
12
|
+
if (!existsSync(VIDEO_DATES_FILE))
|
|
13
|
+
return {};
|
|
14
|
+
try {
|
|
15
|
+
const d = JSON.parse(readFileSync(VIDEO_DATES_FILE, 'utf8'));
|
|
16
|
+
return d && typeof d === 'object' ? d : {};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Merge newly seen videos into the cache (atomic write); no-op when nothing is new. */
|
|
23
|
+
export function rememberVideoDates(videos) {
|
|
24
|
+
const dates = loadVideoDates();
|
|
25
|
+
let changed = false;
|
|
26
|
+
for (const v of videos) {
|
|
27
|
+
if (!v.publishedAt || !Number.isFinite(Date.parse(v.publishedAt)) || dates[v.videoId] === v.publishedAt)
|
|
28
|
+
continue;
|
|
29
|
+
dates[v.videoId] = v.publishedAt;
|
|
30
|
+
changed = true;
|
|
31
|
+
}
|
|
32
|
+
if (!changed)
|
|
33
|
+
return;
|
|
34
|
+
const cutoff = Date.now() - KEEP_DAYS * 24 * 60 * 60 * 1000;
|
|
35
|
+
for (const [id, at] of Object.entries(dates))
|
|
36
|
+
if (Date.parse(at) < cutoff)
|
|
37
|
+
delete dates[id];
|
|
38
|
+
try {
|
|
39
|
+
mkdirSync(dirname(VIDEO_DATES_FILE), { recursive: true });
|
|
40
|
+
const tmp = `${VIDEO_DATES_FILE}.${process.pid}.tmp`;
|
|
41
|
+
writeFileSync(tmp, JSON.stringify(dates));
|
|
42
|
+
renameSync(tmp, VIDEO_DATES_FILE);
|
|
43
|
+
}
|
|
44
|
+
catch { /* best-effort: the guard is only off for dates not written */ }
|
|
45
|
+
}
|
package/dist/lib/yt-lib.js
CHANGED
|
@@ -46,6 +46,24 @@ export function parseState(content) {
|
|
|
46
46
|
export function bumpStateFrontmatterDate(content, date) {
|
|
47
47
|
return content.replace(/^updated:\s*\d{4}-\d{2}-\d{2}/m, `updated: ${date}`);
|
|
48
48
|
}
|
|
49
|
+
/** The state pointer of one type for a channel row (null when unset or the row is missing). */
|
|
50
|
+
export function pointerFor(row, type) {
|
|
51
|
+
if (!row)
|
|
52
|
+
return null;
|
|
53
|
+
return type === 'longform' ? row.last_longform_id : type === 'short' ? row.last_short_id : row.last_live_id;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Would moving the pointer from `pointerId` to `videoId` move it back in time? `dates` maps
|
|
57
|
+
* videoIds to publishedAt. Unknown dates answer false (bump as before): only a known older
|
|
58
|
+
* video is refused. A stale skip applied after the pointer moved on used to rewind it, and
|
|
59
|
+
* the next queue listed already-rated videos again.
|
|
60
|
+
*/
|
|
61
|
+
export function pointerWouldRegress(pointerId, videoId, publishedAt, dates) {
|
|
62
|
+
if (!pointerId || pointerId === videoId || !publishedAt)
|
|
63
|
+
return false;
|
|
64
|
+
const at = dates[pointerId];
|
|
65
|
+
return !!at && Date.parse(at) > Date.parse(publishedAt);
|
|
66
|
+
}
|
|
49
67
|
/**
|
|
50
68
|
* Per-video pointer bump. Updates one type's last_id for one channel.
|
|
51
69
|
* Session counter increments only on first touch of the day (updated != date).
|
|
@@ -19,6 +19,7 @@ import { loadEnv } from "./lib/env.js";
|
|
|
19
19
|
import { parseState } from "./lib/yt-lib.js";
|
|
20
20
|
import { STATE_MD } from "./lib/paths.js";
|
|
21
21
|
import { fetchChannelVideos } from "./lib/yt-api.js";
|
|
22
|
+
import { loadVideoDates, rememberVideoDates } from "./lib/video-dates.js";
|
|
22
23
|
loadEnv();
|
|
23
24
|
const handle = process.argv[2];
|
|
24
25
|
if (!handle) {
|
|
@@ -47,6 +48,10 @@ catch (e) {
|
|
|
47
48
|
console.error(e.message);
|
|
48
49
|
process.exit(1);
|
|
49
50
|
}
|
|
51
|
+
// Date cache read before this listing is added, so a pointer that left the window still has its
|
|
52
|
+
// old date; then every listed video (the pointer too) is remembered for the monotonic bump.
|
|
53
|
+
const videoDates = loadVideoDates();
|
|
54
|
+
rememberVideoDates(videos);
|
|
50
55
|
const pointerByType = {
|
|
51
56
|
longform: row.last_longform_id,
|
|
52
57
|
short: row.last_short_id,
|
|
@@ -74,10 +79,12 @@ for (const type of ['longform', 'short', 'live']) {
|
|
|
74
79
|
}
|
|
75
80
|
// normal: find pointer's publishedAt, take everything strictly newer
|
|
76
81
|
const pointerVideo = ofType.find(v => v.videoId === pointerId);
|
|
82
|
+
// Pointer not in the fetched window: older than the lookback, or deleted/unlisted. Its date
|
|
83
|
+
// from the sweep's cache keeps already-seen videos out; without one, take the whole window.
|
|
84
|
+
const cachedTs = Date.parse(videoDates[pointerId] ?? '');
|
|
77
85
|
const cutoffTs = pointerVideo
|
|
78
86
|
? new Date(pointerVideo.publishedAt).getTime()
|
|
79
|
-
:
|
|
80
|
-
-Infinity;
|
|
87
|
+
: Number.isFinite(cachedTs) ? cachedTs : -Infinity;
|
|
81
88
|
for (const v of ofType) {
|
|
82
89
|
if (new Date(v.publishedAt).getTime() <= cutoffTs)
|
|
83
90
|
continue;
|
package/dist/yt-rating.js
CHANGED
|
@@ -27,7 +27,8 @@ import { spawn } from 'node:child_process';
|
|
|
27
27
|
import { loadEnv } from "./lib/env.js";
|
|
28
28
|
import { loadConfig } from "./lib/config.js";
|
|
29
29
|
import { distillComment } from "./lib/distill.js";
|
|
30
|
-
import { parseChannels, appendSkipTitle, appendNote, bumpStatePointer } from "./lib/yt-lib.js";
|
|
30
|
+
import { parseChannels, parseState, appendSkipTitle, appendNote, bumpStatePointer, pointerFor, pointerWouldRegress } from "./lib/yt-lib.js";
|
|
31
|
+
import { loadVideoDates } from "./lib/video-dates.js";
|
|
31
32
|
import { CHANNELS_MD, STATE_MD, PENDING_FILE, QUEUE_FILE, profilePath } from "./lib/paths.js";
|
|
32
33
|
loadEnv();
|
|
33
34
|
function getArg(args, name) {
|
|
@@ -123,7 +124,14 @@ if (profileAfter !== profileBefore) {
|
|
|
123
124
|
let stateBumped = false;
|
|
124
125
|
if (!args.noState) {
|
|
125
126
|
const stateBefore = readFileSync(STATE_MD, 'utf8');
|
|
126
|
-
|
|
127
|
+
// An older video rated after a newer one of the same channel was title-skipped must not
|
|
128
|
+
// rewind the pointer; the date comes from the sweep's cache of listed videos.
|
|
129
|
+
const pointer = pointerFor(parseState(stateBefore).find(r => r.handle === args.channel), args.type);
|
|
130
|
+
const dates = loadVideoDates();
|
|
131
|
+
const publishedAt = dates[args.id] ?? (() => { const p = loadPending(); return p.videoId === args.id ? p.publishedAt : undefined; })();
|
|
132
|
+
// Never rewind: re-bump the current pointer instead, so the day's session still counts.
|
|
133
|
+
const regress = pointer !== null && pointerWouldRegress(pointer, args.id, publishedAt, dates);
|
|
134
|
+
const stateAfter = bumpStatePointer(stateBefore, args.channel, args.type, regress ? pointer : args.id, date);
|
|
127
135
|
if (stateAfter !== stateBefore) {
|
|
128
136
|
writeFileSync(STATE_MD, stateAfter, 'utf8');
|
|
129
137
|
stateBumped = true;
|
package/dist/yt-sweep.js
CHANGED
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* rating) + `seen`. Foreground is the SOLE writer.
|
|
30
30
|
* Tagged with built_at; auto-rebuilt on a new day or via --reset.
|
|
31
31
|
* - <DATA_DIR>/.cache/queue-rest.json background-fill handoff written by the --fill child.
|
|
32
|
-
* - <DATA_DIR>/.cache/pending.json current ratable video's metadata for yt-rating.ts
|
|
32
|
+
* - <DATA_DIR>/.cache/pending.json current ratable video's metadata for yt-rating.ts, plus its
|
|
33
|
+
* emitted summary so a repeat sweep returns it unchanged.
|
|
33
34
|
* - <DATA_DIR>/.cache/prefetch.json background-computed summary for the NEXT video.
|
|
34
35
|
*
|
|
35
36
|
* All .cache/ files are throwaway (rebuilt each run; safe to delete / gitignore).
|
|
@@ -50,7 +51,8 @@
|
|
|
50
51
|
import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, renameSync, appendFileSync } from 'node:fs';
|
|
51
52
|
import { spawn } from 'node:child_process';
|
|
52
53
|
import { loadEnv, missingEnv, missingEnvMessage, REQUIRED_YOUTUBE } from "./lib/env.js";
|
|
53
|
-
import { parseChannels, parseState, bumpStatePointer, isResolved } from "./lib/yt-lib.js";
|
|
54
|
+
import { parseChannels, parseState, bumpStatePointer, isResolved, pointerFor, pointerWouldRegress } from "./lib/yt-lib.js";
|
|
55
|
+
import { loadVideoDates, rememberVideoDates } from "./lib/video-dates.js";
|
|
54
56
|
import { chat, claudeMissing } from "./lib/llm.js";
|
|
55
57
|
import { outputLang } from "./lib/config.js";
|
|
56
58
|
import { PKG_ROOT, CHANNELS_MD, STATE_MD, CACHE_DIR, QUEUE_FILE, REST_FILE, PENDING_FILE, PREFETCH_FILE, LOG_FILE, profilePath, script, } from "./lib/paths.js";
|
|
@@ -104,9 +106,14 @@ function run(cmd) {
|
|
|
104
106
|
});
|
|
105
107
|
}
|
|
106
108
|
// ---------- state mutation (inline, crash-safe per write) ----------
|
|
107
|
-
function bumpState(handle, type, videoId) {
|
|
109
|
+
function bumpState(handle, type, videoId, publishedAt) {
|
|
108
110
|
const before = readFileSync(STATE_MD, 'utf8');
|
|
109
|
-
const
|
|
111
|
+
const pointer = pointerFor(parseState(before).find(r => r.handle === handle), type);
|
|
112
|
+
// Never rewind: re-bump the current pointer instead, so the day's session still counts.
|
|
113
|
+
const regress = pointer !== null && pointerWouldRegress(pointer, videoId, publishedAt, loadVideoDates());
|
|
114
|
+
if (regress)
|
|
115
|
+
log(`pointer kept ${handle} ${type} ${pointer} (not rewound to older ${videoId})`);
|
|
116
|
+
const after = bumpStatePointer(before, handle, type, regress ? pointer : videoId, today);
|
|
110
117
|
if (after !== before)
|
|
111
118
|
writeFileSync(STATE_MD, after, 'utf8');
|
|
112
119
|
}
|
|
@@ -116,20 +123,23 @@ function bumpState(handle, type, videoId) {
|
|
|
116
123
|
*/
|
|
117
124
|
function persistSkip(item, status) {
|
|
118
125
|
if (status !== 'no_transcript')
|
|
119
|
-
bumpState(item.channel, item.type, item.videoId);
|
|
126
|
+
bumpState(item.channel, item.type, item.videoId, item.publishedAt);
|
|
120
127
|
}
|
|
121
128
|
/**
|
|
122
|
-
* Apply title-skips to state.md.
|
|
129
|
+
* Apply title-skips to state.md. Bumps the pointer (a title-skip is final) unless that would
|
|
130
|
+
* rewind it: a background fill's skips are listed against the pointer it saw when it started.
|
|
123
131
|
* Idempotent — re-applying the same skip is a no-op. ONLY the foreground calls this.
|
|
124
132
|
*/
|
|
125
133
|
function applyTitleSkips(skips) {
|
|
134
|
+
rememberVideoDates(skips);
|
|
126
135
|
for (const s of skips) {
|
|
127
|
-
bumpState(s.channel, s.type, s.videoId);
|
|
136
|
+
bumpState(s.channel, s.type, s.videoId, s.publishedAt);
|
|
128
137
|
recordSkip({ stage: 'title', channel: s.channel, videoId: s.videoId, title: s.title, reason: s.reason || 'title filter' });
|
|
129
138
|
}
|
|
130
139
|
}
|
|
131
140
|
/** Append items, skipping any videoId already queued OR already resolved (seen). */
|
|
132
141
|
function enqueue(queue, add) {
|
|
142
|
+
rememberVideoDates(add);
|
|
133
143
|
const have = new Set([...queue.items.map(i => i.videoId), ...queue.seen]);
|
|
134
144
|
for (const it of add)
|
|
135
145
|
if (!have.has(it.videoId)) {
|
|
@@ -143,22 +153,30 @@ function dropHead(queue) {
|
|
|
143
153
|
if (v)
|
|
144
154
|
queue.seen.push(v.videoId);
|
|
145
155
|
}
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
/**
|
|
157
|
+
* A summary cached today for `videoId` in `file`: prefetch.json (the warmed next video) or
|
|
158
|
+
* pending.json (the head already emitted, so a repeat sweep returns it at once and unchanged).
|
|
159
|
+
*/
|
|
160
|
+
function loadCachedSummary(file, videoId) {
|
|
161
|
+
if (!existsSync(file))
|
|
148
162
|
return null;
|
|
149
163
|
try {
|
|
150
|
-
const p = JSON.parse(readFileSync(
|
|
151
|
-
if (p.videoId === videoId && p.built_at === today)
|
|
164
|
+
const p = JSON.parse(readFileSync(file, 'utf8'));
|
|
165
|
+
if (p.videoId === videoId && p.built_at === today && p.summary)
|
|
152
166
|
return p.summary;
|
|
153
167
|
}
|
|
154
168
|
catch { /* corrupt → ignore */ }
|
|
155
169
|
return null;
|
|
156
170
|
}
|
|
171
|
+
const loadPrefetch = (videoId) => loadCachedSummary(PREFETCH_FILE, videoId);
|
|
157
172
|
/** Atomic write (temp + rename) so a concurrent reader never sees a partial file. */
|
|
173
|
+
function writeAtomic(file, value) {
|
|
174
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
175
|
+
writeFileSync(tmp, JSON.stringify(value));
|
|
176
|
+
renameSync(tmp, file);
|
|
177
|
+
}
|
|
158
178
|
function writePrefetch(p) {
|
|
159
|
-
|
|
160
|
-
writeFileSync(tmp, JSON.stringify(p));
|
|
161
|
-
renameSync(tmp, PREFETCH_FILE);
|
|
179
|
+
writeAtomic(PREFETCH_FILE, p);
|
|
162
180
|
}
|
|
163
181
|
function clearPrefetch() {
|
|
164
182
|
if (existsSync(PREFETCH_FILE))
|
|
@@ -332,7 +350,7 @@ async function expandChannel(ref) {
|
|
|
332
350
|
const skips = [];
|
|
333
351
|
for (const it of candidates) {
|
|
334
352
|
if (titleSkip.has(it.videoId))
|
|
335
|
-
skips.push({ channel: it.channel, type: it.type, videoId: it.videoId, title: it.title, reason: titleSkip.get(it.videoId) || 'title filter' });
|
|
353
|
+
skips.push({ channel: it.channel, type: it.type, videoId: it.videoId, title: it.title, reason: titleSkip.get(it.videoId) || 'title filter', publishedAt: it.publishedAt });
|
|
336
354
|
else
|
|
337
355
|
items.push(it);
|
|
338
356
|
}
|
|
@@ -412,11 +430,7 @@ function loadQueue() {
|
|
|
412
430
|
// ---------- advance (the lazy step) ----------
|
|
413
431
|
/** state.md pointer for this item's type, or null if the channel has no row yet. */
|
|
414
432
|
function statePointer(item) {
|
|
415
|
-
|
|
416
|
-
if (!row)
|
|
417
|
-
return null;
|
|
418
|
-
return item.type === 'longform' ? row.last_longform_id
|
|
419
|
-
: item.type === 'short' ? row.last_short_id : row.last_live_id;
|
|
433
|
+
return pointerFor(parseState(readFileSync(STATE_MD, 'utf8')).find(r => r.handle === item.channel), item.type);
|
|
420
434
|
}
|
|
421
435
|
/**
|
|
422
436
|
* Pure-ish pipeline for one queue item: fetch transcript → substance check + summary.
|
|
@@ -458,10 +472,12 @@ async function advance(queue) {
|
|
|
458
472
|
dropHead(queue);
|
|
459
473
|
continue;
|
|
460
474
|
}
|
|
461
|
-
//
|
|
462
|
-
|
|
475
|
+
// Already emitted (the plugin re-runs the sweep after the briefing turn to open the rating
|
|
476
|
+
// dialog) or warmed by the background child? Use it and skip the live fetch + content filter.
|
|
477
|
+
const emitted = loadCachedSummary(PENDING_FILE, item.videoId);
|
|
478
|
+
const cached = emitted ?? loadPrefetch(item.videoId);
|
|
463
479
|
if (cached)
|
|
464
|
-
log(
|
|
480
|
+
log(`${emitted ? 'pending' : 'prefetch'} hit ${item.videoId}`);
|
|
465
481
|
const result = cached
|
|
466
482
|
? { kind: 'ratable', summary: cached }
|
|
467
483
|
: await processItem(item);
|
|
@@ -485,10 +501,12 @@ async function advance(queue) {
|
|
|
485
501
|
channel: item.channel, videoId: item.videoId, title: item.title,
|
|
486
502
|
type: item.type, publishedAt: item.publishedAt, is_baseline: item.is_baseline,
|
|
487
503
|
};
|
|
488
|
-
|
|
504
|
+
writeAtomic(PENDING_FILE, { ...pending, summary: result.summary, built_at: today });
|
|
489
505
|
writeFileSync(QUEUE_FILE, JSON.stringify(queue));
|
|
490
|
-
// Warm the NEXT video in the background while the user rates this one.
|
|
491
|
-
|
|
506
|
+
// Warm the NEXT video in the background while the user rates this one. A repeat emit of the
|
|
507
|
+
// same head already spawned that child; a second one would summarize the next video twice.
|
|
508
|
+
if (!emitted)
|
|
509
|
+
spawnPrefetch(queue.items[1]);
|
|
492
510
|
emit({ status: 'rating_needed', summary: result.summary, pending, lang: LANG });
|
|
493
511
|
}
|
|
494
512
|
// All processed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-briefing",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "A self-learning YouTube briefing for Claude Code: it sweeps the channels you follow, filters noise in two stages (title, then transcript), gives you a short briefing of the rest in your language in the chat, and adapts to your ratings — one video at a time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -79,26 +79,28 @@ const STRAY_TAP_MS = 1500
|
|
|
79
79
|
|
|
80
80
|
const CHOICES = ['OK', 'Weak', 'Research', 'Stop'] as const
|
|
81
81
|
|
|
82
|
-
/** What
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
/** What the chat shows of each briefing prompt; the instructions ride along as unseen context. */
|
|
83
|
+
const NEXT_TEXT = 'yt: next video'
|
|
84
|
+
/** The first prompt of a /yt run: the only one that rebuilds the queue. */
|
|
85
|
+
const FIRST_TEXT = 'yt: first video'
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* What a briefing turn must do, read from the system prompt while the loop runs. The text is the
|
|
89
|
+
* same for every turn: a session-scoped section may be kept as first composed, so a per-turn
|
|
90
|
+
* `--reset` here once stuck to every turn and threw away the warmed next video each time.
|
|
91
|
+
*/
|
|
92
|
+
function loopSection(): string {
|
|
93
|
+
const sweep = engine('yt-sweep').join(' ')
|
|
85
94
|
return [
|
|
86
|
-
`The yt-briefing plugin is running the person's YouTube briefing. A user message reading exactly "${NEXT_TEXT}" is its request for the next video:`,
|
|
87
|
-
`run \`${sweep}\`; it prints one JSON line. If \`status\` is \`rating_needed\`, answer with \`summary\` verbatim as your whole message, Markdown kept: nothing before or after it, no question, no tool call after it (the plugin opens the rating dialog itself).`,
|
|
95
|
+
`The yt-briefing plugin is running the person's YouTube briefing. A user message reading exactly "${FIRST_TEXT}" or "${NEXT_TEXT}" is its request for the next video:`,
|
|
96
|
+
`for "${FIRST_TEXT}" run \`${sweep} --reset\`, for "${NEXT_TEXT}" run \`${sweep}\`; it prints one JSON line. If \`status\` is \`rating_needed\`, answer with \`summary\` verbatim as your whole message, Markdown kept: nothing before or after it, no question, no tool call after it (the plugin opens the rating dialog itself).`,
|
|
88
97
|
'Otherwise answer with one short line saying why there is nothing to rate. Write in the language of the summaries.',
|
|
89
98
|
].join('\n')
|
|
90
99
|
}
|
|
91
100
|
|
|
92
|
-
/** What the chat shows of each briefing prompt; the instructions ride along as unseen context. */
|
|
93
|
-
const NEXT_TEXT = 'yt: next video'
|
|
94
|
-
|
|
95
|
-
/** Whether the next briefing prompt starts a fresh sweep. */
|
|
96
|
-
let resetNext = false
|
|
97
|
-
|
|
98
101
|
/** Ask for a briefing once the hook that wants it has answered: one submitted from inside would wait on it. */
|
|
99
102
|
function submitLater($: EngineInterface, reset: boolean) {
|
|
100
|
-
|
|
101
|
-
$.clock.after(0, () => void $.prompt.submit({ text: NEXT_TEXT, asUser: true }).catch(err => {
|
|
103
|
+
$.clock.after(0, () => void $.prompt.submit({ text: reset ? FIRST_TEXT : NEXT_TEXT, asUser: true }).catch(err => {
|
|
102
104
|
looping = false
|
|
103
105
|
$.ui.toast(`Could not continue the briefing: ${String(err)}`)
|
|
104
106
|
}))
|
|
@@ -176,7 +178,7 @@ export const register: Register = on => {
|
|
|
176
178
|
on('prompt.compose', async ($, e, next) => {
|
|
177
179
|
const composed = await next(e)
|
|
178
180
|
if (!looping) return composed
|
|
179
|
-
return { sections: [...composed.sections, { id: 'yt-briefing:loop', text: loopSection(
|
|
181
|
+
return { sections: [...composed.sections, { id: 'yt-briefing:loop', text: loopSection(), scope: 'session' as const }] }
|
|
180
182
|
})
|
|
181
183
|
|
|
182
184
|
on('turn.complete', async ($, e, next) => {
|