yt-briefing 1.2.2 → 1.2.4

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/README.md CHANGED
@@ -137,9 +137,10 @@ The **Comment** field takes anything else. Type what you think in your own words
137
137
  shows, skip those") and press Enter: Claude turns it into a standing rule for that channel and
138
138
  infers the rating. `? your question` starts research with that question, `stop` closes the pane.
139
139
  Claude Code on a phone has no text fields, so there the pane shows the four keys only.
140
- Where no pane can be seen (`/yt` sent from claude.ai/code over Remote Control, or a surface that
141
- places no panes), the same loop runs in Claude Code's question dialog: the summary as plain text,
142
- OK / Weak / Research / Stop as choices, and a typed **Other** answer works as the Comment field.
140
+ Where no pane can be seen (`/yt` sent from claude.ai/code or the phone app over Remote Control, or
141
+ a surface that places no panes), `/yt` hands the same loop to Claude in the chat: each summary is
142
+ Claude's message, a short dialog asks for the rating (OK / Weak / Research / Stop), and a typed
143
+ **Other** answer works as the Comment field. This path uses the model, so it counts toward plan usage.
143
144
 
144
145
  Each step is the engine, not a chat turn: rating a video takes no turn and no context of your
145
146
  session, and the next summary is usually ready before you have finished reading the current one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yt-briefing",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
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.2",
3
+ "version": "1.2.4",
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"
@@ -153,64 +153,86 @@ async function comment($: EngineInterface, raw: string) {
153
153
  if (saved?.rule) $.ui.toast(`Rule saved: ${saved.rule}`)
154
154
  }
155
155
 
156
- /** The dialog's choices; anything else is text typed under "Other". */
157
- const CHOICES = ['OK', 'Weak', 'Research', 'Stop'] as const
158
-
159
- /** The dialog draws plain text: drop the briefing's Markdown marks so no `**` or `###` shows. */
160
- function plain(md: string): string {
161
- return md
162
- .replace(/^#{1,6}\s+/gm, '')
163
- .replace(/\*\*(.+?)\*\*/g, '$1')
164
- .replace(/^_(.+)_$/gm, '$1')
165
- .replace(/^(\d+\.)\s+/gm, '$1 ')
166
- .replace(/(\d{4}-\d{2}-\d{2})T[\d:.]+Z/g, '$1')
167
- }
168
-
169
156
  /** True when the person typed /yt through Remote Control and nothing attached can show a pane. */
170
157
  async function paneUnseen($: EngineInterface, origin: { kind: string } | undefined): Promise<boolean> {
171
158
  if (origin?.kind !== 'bridge') return false
172
159
  return !(await $.session.surfaces()).some(s => s !== 'terminal')
173
160
  }
174
161
 
175
- /** The briefing as transcript text: plain, with the filtered-out count at the end. */
176
- function briefing(out: SweepOut): string {
177
- return [plain(out.summary ?? ''), out.skipped ? `(${out.skipped} filtered out)` : undefined]
178
- .filter(line => line !== undefined).join('\n\n')
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
+ /** The prompt for one briefing: the model sweeps and answers with the summary alone. */
176
+ function fetchPrompt(reset: boolean): string {
177
+ const sweep = [...engine('yt-sweep'), ...(reset ? ['--reset'] : [])].join(' ')
178
+ return [
179
+ `Next video of my YouTube briefing: run \`${sweep}\`. It prints one JSON line.`,
180
+ '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 rating dialog follows on its own).',
181
+ 'Otherwise answer with one short line saying why there is nothing to rate. Write in the language of the summaries.',
182
+ ].join('\n')
179
183
  }
180
184
 
181
- /** The rating loop in the engine's question dialog, from a sweep already run, for a session where no pane can be seen. */
182
- async function dialogLoop($: EngineInterface, first: SweepOut) {
183
- let out = first
184
- for (let shown = true; ; shown = false) {
185
- if (out.status !== 'rating_needed' || !out.summary || !out.pending) return void $.ui.log(endText(out))
186
-
187
- // The dialog draws its question in a large heading face, so the briefing goes to the
188
- // transcript as text and the dialog asks only for the rating. The first is the command's output.
189
- if (!shown) $.ui.log(briefing(out))
190
- const question = `${out.pending.channel}: rating?`
191
- let answer: string
185
+ /** Submit a prompt once the hook that wants it has answered: one submitted from inside would wait on it. */
186
+ function submitLater($: EngineInterface, text: string) {
187
+ $.clock.after(0, () => void $.prompt.submit({ text }).catch(err => {
188
+ looping = false
189
+ $.ui.toast(`Could not continue the briefing: ${String(err)}`)
190
+ }))
191
+ }
192
+
193
+ /** After a briefing turn: ask for the rating, record it and fetch the next one. */
194
+ async function rateAfterTurn($: EngineInterface) {
195
+ let out: SweepOut
196
+ try {
197
+ out = await runSweep($, false)
198
+ } catch {
199
+ looping = false
200
+ return
201
+ }
202
+ if (out.status !== 'rating_needed' || !out.pending || !out.summary) {
203
+ looping = false
204
+ return
205
+ }
206
+ const title = out.pending.title.length > 80 ? `${out.pending.title.slice(0, 79)}…` : out.pending.title
207
+ let answer: string
208
+ for (;;) {
209
+ const opened = Date.now()
192
210
  try {
193
- answer = (await $.ui.ask(question, { header: 'yt-briefing', options: CHOICES })).trim()
211
+ answer = (await $.ui.ask(`«${title}» — rating?`, { header: 'yt-briefing', options: CHOICES })).trim()
194
212
  } catch {
213
+ looping = false
195
214
  return
196
215
  }
216
+ if (!looping) return
217
+ if (Date.now() - opened >= STRAY_TAP_MS) break
218
+ }
197
219
 
198
- if (answer === 'Stop' || answer.toLowerCase() === 'stop' || !answer) return
199
- if (answer === 'Research' || answer.startsWith('?')) {
200
- if (!(await record($, ['--rating', '1']))) return
201
- const q = answer.startsWith('?') ? answer.slice(1).trim() || undefined : undefined
202
- return handOff($, out.pending, out.summary, out.lang, q)
203
- }
204
- const args = answer === 'OK' ? ['--rating', '1'] : answer === 'Weak' ? ['--rating', '0'] : ['--raw-comment', answer]
205
- const saved = await record($, args)
206
- if (!saved) return
207
- $.ui.log(saved.rule ? `Rule saved: ${saved.rule}. Looking for the next video…` : 'Saved. Looking for the next video…')
208
- try {
209
- out = await runSweep($, false)
210
- } catch (err) {
211
- return void $.ui.log(`The engine did not answer: ${String(err)}`)
212
- }
220
+ if (answer === 'Stop' || answer.toLowerCase() === 'stop' || !answer) {
221
+ looping = false
222
+ return
223
+ }
224
+ if (answer === 'Research' || answer.startsWith('?')) {
225
+ looping = false
226
+ if (!(await record($, ['--rating', '1']))) return
227
+ const q = answer.startsWith('?') ? answer.slice(1).trim() || undefined : undefined
228
+ return handOff($, out.pending, out.summary, out.lang, q)
229
+ }
230
+ const args = answer === 'OK' ? ['--rating', '1'] : answer === 'Weak' ? ['--rating', '0'] : ['--raw-comment', answer]
231
+ if (!(await record($, args))) {
232
+ looping = false
233
+ return
213
234
  }
235
+ submitLater($, fetchPrompt(false))
214
236
  }
215
237
 
216
238
  export const register: Register = on => {
@@ -232,18 +254,20 @@ export const register: Register = on => {
232
254
  }
233
255
  await $.ui.close({ id: PANE })
234
256
  }
235
- // No pane to show progress in: the command itself runs while the first sweep does, so the
236
- // session shows it working, and only then hands over to the dialogs.
237
- let first: SweepOut
238
- try {
239
- first = await runSweep($, true)
240
- } catch (err) {
241
- return { text: `The engine did not answer: ${String(err)}` }
242
- }
243
- if (first.status !== 'rating_needed') return { text: endText(first) }
244
- void dialogLoop($, first)
257
+ looping = true
258
+ submitLater($, fetchPrompt(true))
259
+
260
+ return { text: 'No pane here: the briefing runs in the chat.' }
261
+ })
245
262
 
246
- return { text: briefing(first) }
263
+ on('prompt.submit', async ($, e, next) => {
264
+ if (looping && e.origin?.kind !== 'plugin') looping = false
265
+ return next(e)
266
+ })
267
+
268
+ on('turn.complete', async ($, e, next) => {
269
+ if (looping && !e.agentId && e.reason === 'answer') void rateAfterTurn($)
270
+ return next(e)
247
271
  })
248
272
 
249
273
  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {