yt-briefing 1.2.3 → 1.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yt-briefing",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
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 a pane, and adapts to your ratings — one video at a time.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yt-briefing",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "The /yt briefing loop as a Claude Code pane: summary, one-key rating, research hand-off.",
5
5
  "author": {
6
6
  "name": "Michal Ryzio"
@@ -159,30 +159,89 @@ async function paneUnseen($: EngineInterface, origin: { kind: string } | undefin
159
159
  return !(await $.session.surfaces()).some(s => s !== 'terminal')
160
160
  }
161
161
 
162
- /**
163
- * The loop for a session where no pane can be seen, handed to the session as a prompt. A plugin
164
- * cannot show a second transcript row a remote client draws (it cannot run its own command, and
165
- * its log lines reach only the terminal), so the briefing is the model's own message text and the
166
- * dialog asks for the rating alone: its question is drawn in a heading face, too large for a briefing.
167
- */
168
- function remoteLoop(): string {
169
- const sweep = engine('yt-sweep').join(' ')
170
- const rating = engine('yt-rating').join(' ')
171
- const transcript = [...engine('yt-transcript'), '<videoId>', '--lang', 'auto'].join(' ')
162
+ // Where no pane can be seen the loop runs in the chat. A phone or web client draws a plugin's
163
+ // command output and its dialogs, but not its log lines, and it folds text written mid-turn into a
164
+ // one-line digest; only a turn's last message shows whole. So each briefing is the model's whole
165
+ // answer to a prompt this plugin submits, and the rating dialog opens once that turn has ended.
166
+
167
+ /** True while the chat loop runs; a prompt the person types ends it. */
168
+ let looping = false
169
+
170
+ /** An answer this soon after its dialog opened is a tap meant for the dialog before it. */
171
+ const STRAY_TAP_MS = 1500
172
+
173
+ const CHOICES = ['OK', 'Weak', 'Research', 'Stop'] as const
174
+
175
+ /** What a briefing turn must do, read from the system prompt while the loop runs. */
176
+ function loopSection(reset: boolean): string {
177
+ const sweep = [...engine('yt-sweep'), ...(reset ? ['--reset'] : [])].join(' ')
172
178
  return [
173
- 'Run my YouTube briefing here in the chat, one video at a time, until I stop or nothing is left.',
174
- '',
175
- `1. Run \`${sweep} --reset\` the first time, \`${sweep}\` after that. It prints one JSON line.`,
176
- '2. When `status` is `rating_needed`: post `summary` as your whole message, verbatim, Markdown kept. Add nothing before or after it.',
177
- '3. Then call AskUserQuestion with one question, exactly `«<pending.title>» — rating?` (nothing else in it), header `yt-briefing`, options OK, Weak, Research, Stop, no descriptions or previews.',
178
- `4. OK: \`${rating} --rating 1\`. Weak: \`${rating} --rating 0\`. Text typed under Other: \`${rating} --raw-comment "<the text>"\`. Then back to step 1, without a word in between.`,
179
- `5. Research: \`${rating} --rating 1\`, then research the video with me: ask what I want to dig into, pull the transcript with \`${transcript}\` when needed (never paste it), keep what the video claims apart from what you verify.`,
180
- '6. Stop, or any other `status`: say so in one short line and end.',
181
- '',
182
- 'Write in the language of the summaries.',
179
+ `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:`,
180
+ `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).`,
181
+ 'Otherwise answer with one short line saying why there is nothing to rate. Write in the language of the summaries.',
183
182
  ].join('\n')
184
183
  }
185
184
 
185
+ /** What the chat shows of each briefing prompt; the instructions ride along as unseen context. */
186
+ const NEXT_TEXT = 'yt: next video'
187
+
188
+ /** Whether the next briefing prompt starts a fresh sweep. */
189
+ let resetNext = false
190
+
191
+ /** Ask for a briefing once the hook that wants it has answered: one submitted from inside would wait on it. */
192
+ function submitLater($: EngineInterface, reset: boolean) {
193
+ resetNext = reset
194
+ $.clock.after(0, () => void $.prompt.submit({ text: NEXT_TEXT, asUser: true }).catch(err => {
195
+ looping = false
196
+ $.ui.toast(`Could not continue the briefing: ${String(err)}`)
197
+ }))
198
+ }
199
+
200
+ /** After a briefing turn: ask for the rating, record it and fetch the next one. */
201
+ async function rateAfterTurn($: EngineInterface) {
202
+ let out: SweepOut
203
+ try {
204
+ out = await runSweep($, false)
205
+ } catch {
206
+ looping = false
207
+ return
208
+ }
209
+ if (out.status !== 'rating_needed' || !out.pending || !out.summary) {
210
+ looping = false
211
+ return
212
+ }
213
+ const title = out.pending.title.length > 80 ? `${out.pending.title.slice(0, 79)}…` : out.pending.title
214
+ let answer: string
215
+ for (;;) {
216
+ const opened = Date.now()
217
+ try {
218
+ answer = (await $.ui.ask(`«${title}» — rating?`, { header: 'yt-briefing', options: CHOICES })).trim()
219
+ } catch {
220
+ looping = false
221
+ return
222
+ }
223
+ if (!looping) return
224
+ if (Date.now() - opened >= STRAY_TAP_MS) break
225
+ }
226
+
227
+ if (answer === 'Stop' || answer.toLowerCase() === 'stop' || !answer) {
228
+ looping = false
229
+ return
230
+ }
231
+ if (answer === 'Research' || answer.startsWith('?')) {
232
+ looping = false
233
+ if (!(await record($, ['--rating', '1']))) return
234
+ const q = answer.startsWith('?') ? answer.slice(1).trim() || undefined : undefined
235
+ return handOff($, out.pending, out.summary, out.lang, q)
236
+ }
237
+ const args = answer === 'OK' ? ['--rating', '1'] : answer === 'Weak' ? ['--rating', '0'] : ['--raw-comment', answer]
238
+ if (!(await record($, args))) {
239
+ looping = false
240
+ return
241
+ }
242
+ submitLater($, false)
243
+ }
244
+
186
245
  export const register: Register = on => {
187
246
  on('session.start', async ($, e, next) => {
188
247
  await $.command.register({
@@ -202,12 +261,30 @@ export const register: Register = on => {
202
261
  }
203
262
  await $.ui.close({ id: PANE })
204
263
  }
205
- // A prompt submitted while this hook holds the command would wait on it: submit once it has answered.
206
- $.clock.after(0, () => void $.prompt.submit({ text: remoteLoop() }).catch(err => $.ui.toast(`Could not start the briefing: ${String(err)}`)))
264
+ looping = true
265
+ submitLater($, true)
207
266
 
208
267
  return { text: 'No pane here: the briefing runs in the chat.' }
209
268
  })
210
269
 
270
+ // The engine shows a plugin none of its own prompts, so a prompt seen here is the person's.
271
+ on('prompt.submit', async ($, e, next) => {
272
+ if (looping && e.origin?.kind !== 'plugin') looping = false
273
+ return next(e)
274
+ })
275
+
276
+ // The loop's instructions ride in the system prompt, so the chat shows only NEXT_TEXT.
277
+ on('prompt.compose', async ($, e, next) => {
278
+ const composed = await next(e)
279
+ if (!looping) return composed
280
+ return { sections: [...composed.sections, { id: 'yt-briefing:loop', text: loopSection(resetNext), scope: 'session' as const }] }
281
+ })
282
+
283
+ on('turn.complete', async ($, e, next) => {
284
+ if (looping && !e.agentId && e.reason === 'answer') void rateAfterTurn($)
285
+ return next(e)
286
+ })
287
+
211
288
  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
212
289
  const { Box, Text, Button, Markdown } = $.ui.resolve(e)
213
290
  const v = await read($, view)