yt-briefing 1.2.5 → 1.3.1

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
@@ -16,8 +16,8 @@ It also gets better the more you use it. You give each summary a quick rating, w
16
16
  or not, and from that it learns what to keep showing you and what to drop. Over time the queue
17
17
  becomes yours: less noise, more of what you care about.
18
18
 
19
- yt-briefing runs inside [Claude Code](https://claude.com/claude-code). `/yt` opens the briefing in
20
- a pane next to your chat, and the filtering and summaries run on your own Claude Code login. There
19
+ yt-briefing runs inside [Claude Code](https://claude.com/claude-code). `/yt` runs the briefing in
20
+ your chat, and the filtering and summaries run on your own Claude Code login. There
21
21
  is no separate model, provider or LLM key to set up.
22
22
 
23
23
  ## First run vs later
@@ -120,32 +120,25 @@ it to go deeper, lower it for a quicker pass:
120
120
 
121
121
  ## Run it
122
122
 
123
- Open your project in Claude Code and type `/yt`. The briefing opens in a pane: the summary, and
124
- under it four keys.
125
-
126
- The keys work while the pane has the focus (it takes it when it opens, `/yt` from an empty
127
- prompt), and Esc closes it.
128
-
129
- | Key | What it does |
130
- |-----|--------------|
131
- | `1` OK | Neutral. The video is marked as seen, the next one loads. |
132
- | `2` Weak | Worthless. The title goes to the channel's skip examples, so the filter learns to drop titles like it. |
133
- | `3` Research | Ends the loop and hands this video to Claude, see below. |
134
- | `4` Stop | Closes the pane. The next `/yt` resumes where you stopped. |
135
-
136
- The **Comment** field takes anything else. Type what you think in your own words ("too many panel
137
- shows, skip those") and press Enter: Claude turns it into a standing rule for that channel and
138
- infers the rating. `? your question` starts research with that question, `stop` closes the pane.
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 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.
144
-
145
- Each step is the engine, not a chat turn: rating a video takes no turn and no context of your
146
- session, and the next summary is usually ready before you have finished reading the current one.
147
- The summaries themselves still count toward your Claude plan's usage, like any other Claude Code
148
- work.
123
+ Open your project in Claude Code and type `/yt`. Each summary arrives as Claude's message in the
124
+ chat, and a short dialog under it asks for the rating:
125
+
126
+ | Answer | What it does |
127
+ |--------|--------------|
128
+ | OK | Neutral. The video is marked as seen, the next one loads. |
129
+ | Weak | Worthless. The title goes to the channel's skip examples, so the filter learns to drop titles like it. |
130
+ | Research | Ends the loop and hands this video to Claude, see below. |
131
+ | Stop | Ends the loop. The next `/yt` resumes where you stopped. |
132
+
133
+ The dialog's **Other** field takes anything else. Type what you think in your own words ("too many
134
+ panel shows, skip those"): Claude turns it into a standing rule for that channel and infers the
135
+ rating. `? your question` starts research with that question, `stop` ends the loop. A prompt you
136
+ type in the chat also ends it.
137
+
138
+ It works the same in the terminal, the desktop app, claude.ai/code and the phone app. Each summary
139
+ is a turn of its own that ends with the summary, so the phone app shows it whole instead of folding
140
+ it into a one-line digest. Claude pastes the summary the engine wrote, so a turn is short, but it
141
+ still counts toward your Claude plan's usage, like the summaries themselves.
149
142
 
150
143
  `/yt` is a Claude Code mod (a plugin in `.claude/skills/yt-briefing/`). Claude Code loads it on
151
144
  its own once you trust the project folder. If `/yt` is not listed, start a fresh session. To
@@ -156,8 +149,8 @@ installs `/yt`, `/yt-transcribe` and `/yt-search`).
156
149
 
157
150
  Tech channels announce something new every week, and the usual fate is "looks interesting" →
158
151
  to-do list → never. So next to OK/Weak there is a third key: **Research**. Press it, or type
159
- `? your question` into the comment field, and the loop ends there: the pane closes and the video
160
- lands in your chat with its briefing and the command for its full transcript. Claude works your
152
+ `? your question` into the dialog's Other field, and the loop ends there: the video lands in your
153
+ chat with its briefing and the command for its full transcript. Claude works your
161
154
  question with you, against your own codebase if you ask "would this fit my project", against the
162
155
  web if the claims need checking. A quick feedback loop instead of a shelf. The video is marked as
163
156
  seen, and the next `/yt` resumes the queue right where you broke off.
@@ -166,8 +159,8 @@ seen, and the next `/yt` resumes the queue right where you broke off.
166
159
 
167
160
  1.0 runs on Claude Code only. The OpenAI-compatible provider and its three `YT_BRIEFING_LLM_*`
168
161
  keys are gone (delete them from `.env`), and the chat-driven `/yt` skill with its rating popup is
169
- replaced by the pane. Run `npx yt-briefing install-skill` once in your project: it installs the
170
- pane, and removes the old `/yt` skill and the summary-gate hook from `.claude/settings.json`. Your
162
+ replaced by the `/yt` mod. Run `npx yt-briefing install-skill` once in your project: it installs
163
+ the mod, and removes the old `/yt` skill and the summary-gate hook from `.claude/settings.json`. Your
171
164
  channels, profiles and ratings in `.yt-briefing/data/` carry over unchanged.
172
165
 
173
166
  ## Why Claude Code, and nothing else
@@ -179,13 +172,12 @@ is set in your environment, the engine removes it for that call, because Claude
179
172
  otherwise bill it as API usage instead of using your login.
180
173
 
181
174
  The engine still works ahead in the background. It expands channels in parallel and summarizes the
182
- next video while you rate the current one, so each step is usually ready with no wait. The pane
183
- only shows what the engine produced and sends your key presses back to it.
175
+ next video while you rate the current one, so each step is usually ready with no wait. The mod
176
+ asks for the rating and records it itself, so the model never has to remember the protocol.
184
177
 
185
- Supporting every agent that reads `SKILL.md` meant a chat loop for the rating: the model pasted
186
- each summary, asked the question and recorded the answer, every video a full turn, plus a hook to
187
- make sure the summary was really shown. A Claude Code pane does the same with no model in the loop,
188
- which is why 1.0 drops the other agents.
178
+ Supporting every agent that reads `SKILL.md` meant leaving the rating to the model: it asked the
179
+ question and recorded the answer, plus a hook to make sure the summary was really shown. A Claude
180
+ Code mod drives the loop itself, which is why 1.0 drops the other agents.
189
181
 
190
182
  ## Why one transcript at a time
191
183
 
package/dist/bootstrap.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * 1. Output language for summaries + ratings → DATA_DIR/config.json
9
9
  * 2. The channels you follow — just a flat list of handles
10
10
  * → DATA_DIR/channels.md, DATA_DIR/state.md, DATA_DIR/channels/<slug>.md
11
- * 3. Installs /yt (the rating pane) + /yt-transcribe + /yt-search into this Claude Code project
11
+ * 3. Installs /yt (the rating loop) + /yt-transcribe + /yt-search into this Claude Code project
12
12
  *
13
13
  * It does NOT touch keys: the one key (YouTube Data API) lives in your project root .env (see README →
14
14
  * Setup); the engine reads it at run time. Filters and summaries run on your Claude Code login. Re-running is safe: it warns before overwriting existing data and bails.
@@ -124,7 +124,7 @@ function main() {
124
124
  console.log(`\n ⚠ ${noClaude}`);
125
125
  console.log('\n Next:');
126
126
  console.log(' 1. Open this folder in Claude Code (trust it when asked).');
127
- console.log(' 2. Type /yt — the briefing opens in a pane. (/yt-transcribe <url> for one video)\n');
127
+ console.log(' 2. Type /yt — the briefing runs in the chat. (/yt-transcribe <url> for one video)\n');
128
128
  }
129
129
  try {
130
130
  main();
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * or Bun), passing remaining args through.
7
7
  *
8
8
  * yt-briefing init interactive onboarding wizard
9
- * yt-briefing install-skill install /yt (pane) + skills into a Claude Code project
9
+ * yt-briefing install-skill install /yt + skills into a Claude Code project
10
10
  * yt-briefing add|remove <@handle|url> add or remove channels (also list)
11
11
  * yt-briefing list list the channels you follow
12
12
  * yt-briefing sweep [--reset] advance one step; prints a JSON status line
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * install-skill — install yt-briefing into a Claude Code project: the `/yt` mod (the rating pane)
3
+ * install-skill — install yt-briefing into a Claude Code project: the `/yt` mod (the rating loop)
4
4
  * plus the `/yt-transcribe` and `/yt-search` skills, all under `<project>/.claude/skills/`.
5
5
  *
6
6
  * yt-briefing install-skill # interactive: which project folder
@@ -13,7 +13,7 @@
13
13
  import { installAll, isPackageDevCwd } from "./lib/skill-install.js";
14
14
  import { question } from "./lib/prompt.js";
15
15
  const ask = (q, def = '') => question(def ? `${q} [${def}]:` : `${q}:`).trim() || def;
16
- console.log('\n Install /yt (pane) + /yt-transcribe + /yt-search into a Claude Code project.\n');
16
+ console.log('\n Install /yt + /yt-transcribe + /yt-search into a Claude Code project.\n');
17
17
  console.log(' 1) This project (current folder) — recommended');
18
18
  console.log(' 2) Another project folder\n');
19
19
  const other = ask(' Where', '1') === '2';
@@ -2,8 +2,8 @@
2
2
  * Turn a raw rating comment ("too much politics, skip these panels") into what the profile
3
3
  * stores: one clean, generalizable rule for `## Notes`, plus the rating it implies.
4
4
  *
5
- * This used to be the agent's job in the chat loop. With the rating loop in a pane there is no
6
- * agent turn per video, so the engine does it with the same `claude -p` call it uses everywhere.
5
+ * This used to be the agent's job in the chat loop. The mod records ratings itself, with no agent
6
+ * step for it, so the engine does it with the same `claude -p` call it uses everywhere.
7
7
  */
8
8
  import { chat } from "./llm.js";
9
9
  export function distillPrompt(comment, video) {
@@ -6,7 +6,7 @@
6
6
  * `<project>/.claude/skills/`:
7
7
  *
8
8
  * yt-briefing/ the Claude Code mod (a plugin of function hooks) behind `/yt`: the rating loop
9
- * as a pane. Claude Code loads a plugin from the project's skills folder by itself
9
+ * in the chat. Claude Code loads a plugin from the project's skills folder by itself
10
10
  * (as `yt-briefing@skills-dir`) once the workspace is trusted.
11
11
  * yt-transcribe/ one-shot skill: a single video's transcript → summary.
12
12
  * yt-search/ skill: search within one channel → triage → comparison.
@@ -79,7 +79,7 @@ export function skillBody(name, dist = false) {
79
79
  .replace(/data\//g, toProjectRel(DATA_DIR) + '/');
80
80
  }
81
81
  /**
82
- * The mod's `hooks/engine.ts`: how the pane runs the engine. `dist=false` is the shipped dev form;
82
+ * The mod's `hooks/engine.ts`: how the mod runs the engine. `dist=false` is the shipped dev form;
83
83
  * `dist=true` points at the compiled scripts, relative to the project root (the session's cwd).
84
84
  */
85
85
  export function engineModule(dist = false) {
@@ -88,7 +88,7 @@ export function engineModule(dist = false) {
88
88
  return shipped;
89
89
  const rel = toProjectRel(DIST_DIR);
90
90
  return [
91
- '// How the pane runs the engine, relative to the project root (the session\'s working directory).',
91
+ '// How the mod runs the engine, relative to the project root (the session\'s working directory).',
92
92
  '// Written by `yt-briefing install-skill`; re-run it after moving the project or switching runtime.',
93
93
  `export const engine = (name: string): string[] => ['${runtimeName()}', \`${rel}/\${name}.js\`]`,
94
94
  '',
package/dist/yt-sweep.js CHANGED
@@ -296,6 +296,7 @@ Steps:
296
296
  - 2-5 numbered thematic sections × 2-5 sentences each
297
297
  - At most 5-8 short quotes from the transcript
298
298
  - No timestamps
299
+ - No section of your own on the material's credibility, sourcing or bias (no "reliability notes", no "caveats"): report what the video says. Only a channel directive can ask for such an assessment.
299
300
  3. Apply the channel directives above, if any — they shape the briefing's content, emphasis, and tone, but not the header/subtitle/section skeleton. Put any directive-driven assessment in its own short, clearly-labeled section.
300
301
  4. Language: natural ${LANG}. Avoid calques/anglicisms; use foreign words only for proper nouns or established technical terms. Section headers should be verb phrases, not noun stacks.
301
302
  5. Output: ONLY the briefing itself OR 'OFFTOPIC: ...'. No preamble, and no meta-commentary about these instructions.`;
@@ -99,7 +99,7 @@ version, so every machine gets the setting):
99
99
  }
100
100
  ```
101
101
 
102
- The engine runs `after_rate` after every recorded rating, from the pane and from the CLI
102
+ The engine runs `after_rate` after every recorded rating, from `/yt` and from the CLI
103
103
  (`yt-briefing rate`) alike. It runs detached from the project root, so a slow push never holds the
104
104
  next video, and a failing script never fails the rating (the rating is already on disk). The
105
105
  engine itself still never runs git: the command is yours.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yt-briefing",
3
- "version": "1.2.5",
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.",
3
+ "version": "1.3.1",
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": {
7
7
  "yt-briefing": "dist/cli.js"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yt-briefing",
3
- "version": "1.2.5",
4
- "description": "The /yt briefing loop as a Claude Code pane: summary, one-key rating, research hand-off.",
3
+ "version": "1.3.0",
4
+ "description": "The /yt briefing loop in the chat: summary, rating dialog, research hand-off.",
5
5
  "author": {
6
6
  "name": "Michal Ryzio"
7
7
  },
@@ -1,4 +1,4 @@
1
- // How the pane runs the engine, relative to the project root (the session's working directory).
1
+ // How the mod runs the engine, relative to the project root (the session's working directory).
2
2
  // This is the dev-clone form; `yt-briefing install-skill` / `init` rewrites it for the project it
3
3
  // installs into (the compiled dist/ under node_modules, with the runtime that ran the installer).
4
4
  export const engine = (name: string): string[] => ['bun', `src/${name}.ts`]
@@ -1,38 +1,23 @@
1
- import { atom, read, update } from 'claude-code'
2
1
  import type { EngineInterface, Register } from 'claude-code'
3
2
 
4
- import type { Pending, View } from '../types'
3
+ import type { Pending } from '../types'
5
4
  import { engine } from './engine.ts'
6
5
 
7
- // The /yt rating loop as a pane. Every step runs the engine as a subprocess and reads its one-line
8
- // JSON; the model is not involved unless the person asks for research, which hands the video to
9
- // the session as a prompt. Ratings and comments are written by the engine, never by this module.
10
- // Where no pane can be seen (a Remote Control web client, a surface that places none) the same
11
- // loop runs in the engine's own question dialog instead.
12
-
13
- const PANE = 'yt-briefing'
14
- const view = atom({ plugin: 'yt-briefing', key: 'view' } as const, { phase: 'idle' } as View)
6
+ // The /yt rating loop, in the chat on every surface. Each briefing is the model's whole answer to a
7
+ // prompt this plugin submits, so the summary shows in full in the terminal, the desktop app and on
8
+ // a phone; the rating dialog opens once that turn has ended. Ratings and comments are written by
9
+ // the engine, never by this module.
15
10
 
16
11
  /** A sweep can expand channels, fetch transcripts and summarize; give it the engine's full budget. */
17
12
  const SWEEP_MS = 10 * 60_000
18
13
  /** A rating is a file write; a raw comment adds one `claude -p` call to distill it. */
19
14
  const RATE_MS = 3 * 60_000
20
15
 
21
- type Skip = { channel: string; title: string; reason: string }
22
16
  type SweepOut = {
23
17
  status: string
24
18
  summary?: string
25
19
  pending?: Pending
26
20
  lang?: string
27
- error?: string
28
- skipped?: number
29
- skips?: Skip[]
30
- }
31
-
32
- const STATUS_TEXT: Record<string, string> = {
33
- done: 'Sweep finished: nothing left to rate.',
34
- rate_limited: 'YouTube is blocking this IP (429 / captcha). See README → Proxy.',
35
- tooling_error: 'Transcript tooling failed (proxy, yt-dlp or network). See README → Proxy.',
36
21
  }
37
22
 
38
23
  /** The engine prints one JSON line on stdout; take the last non-empty line to be safe. */
@@ -41,64 +26,12 @@ function parseOut(stdout: string): SweepOut {
41
26
  return JSON.parse(line) as SweepOut
42
27
  }
43
28
 
44
- /** Replace the whole view; the one writer for a fresh phase. */
45
- async function setView($: EngineInterface, next: View) {
46
- await update($, view, () => next)
47
- }
48
-
49
- function skipLine(out: SweepOut): string | undefined {
50
- if (!out.skipped || !out.skips?.length) return undefined
51
- const list = out.skips.map(s => `${s.channel} «${s.title}» — ${s.reason}`).join('; ')
52
- return `Skipped ${out.skipped}: ${list}`
53
- }
54
-
55
29
  /** One engine sweep: the next ratable video, or the end state. Throws when the engine does not answer. */
56
- async function runSweep($: EngineInterface, reset: boolean): Promise<SweepOut> {
57
- const run = await $.process.run([...engine('yt-sweep'), ...(reset ? ['--reset'] : [])], { timeoutMs: SWEEP_MS })
30
+ async function runSweep($: EngineInterface): Promise<SweepOut> {
31
+ const run = await $.process.run(engine('yt-sweep'), { timeoutMs: SWEEP_MS })
58
32
  return parseOut(run.stdout)
59
33
  }
60
34
 
61
- /** What a sweep that found nothing to rate says, skips included. */
62
- function endText(out: SweepOut): string {
63
- const text = out.status === 'done' ? STATUS_TEXT.done : out.error ?? STATUS_TEXT[out.status] ?? `Engine status: ${out.status}`
64
- const skipped = skipLine(out)
65
- return skipped ? `${text}\n${skipped}` : text
66
- }
67
-
68
- /** Advance to the next ratable video and put it (or the end state) in the view. */
69
- async function sweep($: EngineInterface, reset: boolean) {
70
- await setView($, { phase: 'loading' })
71
- let out: SweepOut
72
- try {
73
- out = await runSweep($, reset)
74
- } catch (err) {
75
- await setView($, { phase: 'error', message: `The engine did not answer: ${String(err)}` })
76
- return
77
- }
78
- const skipped = skipLine(out)
79
- if (out.status === 'rating_needed' && out.summary && out.pending) {
80
- await setView($, { phase: 'rating', summary: out.summary, pending: out.pending, lang: out.lang, skipped })
81
- } else if (out.status === 'done') {
82
- await setView($, { phase: 'done', message: STATUS_TEXT.done, skipped })
83
- } else {
84
- await setView($, { phase: 'error', message: out.error ?? STATUS_TEXT[out.status] ?? `Engine status: ${out.status}`, skipped })
85
- }
86
- }
87
-
88
- /** Record a rating (engine args), then move on; on failure stay on the video and say why. */
89
- async function rate($: EngineInterface, args: string[], thenNext = true): Promise<{ rule?: string } | null> {
90
- const v = await read($, view)
91
- if (v.phase !== 'rating' || v.busy) return null
92
- await update($, view, cur => ({ ...cur, busy: args[0] === '--raw-comment' ? 'Turning the comment into a rule…' : 'Saving…' }))
93
- const result = await record($, args)
94
- if (!result) {
95
- await update($, view, cur => ({ ...cur, busy: undefined }))
96
- return null
97
- }
98
- if (thenNext) void sweep($, false)
99
- return result
100
- }
101
-
102
35
  /** Run the rating engine; on failure toast why and resolve null. */
103
36
  async function record($: EngineInterface, args: string[]): Promise<{ rule?: string } | null> {
104
37
  const run = await $.process.run([...engine('yt-rating'), ...args], { timeoutMs: RATE_MS }).catch(err => ({
@@ -113,15 +46,6 @@ async function record($: EngineInterface, args: string[]): Promise<{ rule?: stri
113
46
  return result
114
47
  }
115
48
 
116
- /** Research: mark the video seen, close the pane and hand the video to the session. */
117
- async function research($: EngineInterface, question?: string) {
118
- const v = await read($, view)
119
- if (!v.pending || !v.summary) return
120
- if (!(await rate($, ['--rating', '1'], false))) return
121
- await $.ui.close({ id: PANE })
122
- await handOff($, v.pending, v.summary, v.lang, question)
123
- }
124
-
125
49
  /** Give the video to the session as the person's prompt, to research it together. */
126
50
  async function handOff($: EngineInterface, p: Pending, summary: string, lang: string | undefined, question?: string) {
127
51
  const transcript = [...engine('yt-transcript'), p.videoId, '--lang', 'auto'].join(' ')
@@ -144,25 +68,8 @@ async function handOff($: EngineInterface, p: Pending, summary: string, lang: st
144
68
  }).catch(err => $.ui.toast(`Could not hand the video to the session: ${String(err)}`))
145
69
  }
146
70
 
147
- async function comment($: EngineInterface, raw: string) {
148
- const text = raw.trim()
149
- if (!text) return
150
- if (text.toLowerCase() === 'stop') return void $.ui.close({ id: PANE })
151
- if (text.startsWith('?')) return research($, text.slice(1).trim() || undefined)
152
- const saved = await rate($, ['--raw-comment', text])
153
- if (saved?.rule) $.ui.toast(`Rule saved: ${saved.rule}`)
154
- }
155
-
156
- /** True when the person typed /yt through Remote Control and nothing attached can show a pane. */
157
- async function paneUnseen($: EngineInterface, origin: { kind: string } | undefined): Promise<boolean> {
158
- if (origin?.kind !== 'bridge') return false
159
- return !(await $.session.surfaces()).some(s => s !== 'terminal')
160
- }
161
-
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.
71
+ // A phone or web client draws a plugin's command output and its dialogs, but not its log lines,
72
+ // and it folds text written mid-turn into a one-line digest; only a turn's last message shows whole.
166
73
 
167
74
  /** True while the chat loop runs; a prompt the person types ends it. */
168
75
  let looping = false
@@ -201,7 +108,7 @@ function submitLater($: EngineInterface, reset: boolean) {
201
108
  async function rateAfterTurn($: EngineInterface) {
202
109
  let out: SweepOut
203
110
  try {
204
- out = await runSweep($, false)
111
+ out = await runSweep($)
205
112
  } catch {
206
113
  looping = false
207
114
  return
@@ -246,25 +153,17 @@ export const register: Register = on => {
246
153
  on('session.start', async ($, e, next) => {
247
154
  await $.command.register({
248
155
  name: 'yt',
249
- description: 'Rate the next videos from the YouTube channels you follow, in a pane',
156
+ description: 'Rate the next videos from the YouTube channels you follow',
250
157
  })
251
158
 
252
159
  return next(e)
253
160
  })
254
161
 
255
- on('command.run', { command: 'yt' }, async ($, e) => {
256
- if (!(await paneUnseen($, e.origin))) {
257
- const opened = await $.ui.open({ id: PANE, title: 'yt-briefing', focus: true, closeOnEscape: true })
258
- if (opened.isPlaced) {
259
- void sweep($, true)
260
- return { text: 'Briefing opened in a pane.' }
261
- }
262
- await $.ui.close({ id: PANE })
263
- }
162
+ on('command.run', { command: 'yt' }, async ($) => {
264
163
  looping = true
265
164
  submitLater($, true)
266
165
 
267
- return { text: 'No pane here: the briefing runs in the chat.' }
166
+ return { text: 'The briefing runs in the chat.' }
268
167
  })
269
168
 
270
169
  // The engine shows a plugin none of its own prompts, so a prompt seen here is the person's.
@@ -284,57 +183,4 @@ export const register: Register = on => {
284
183
  if (looping && !e.agentId && e.reason === 'answer') void rateAfterTurn($)
285
184
  return next(e)
286
185
  })
287
-
288
- on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
289
- const { Box, Text, Button, Markdown } = $.ui.resolve(e)
290
- const v = await read($, view)
291
- const close = () => void $.ui.close({ id: PANE })
292
-
293
- if (v.phase === 'idle' || v.phase === 'loading') {
294
- return <Text dimColor>Finding the next video…</Text>
295
- }
296
-
297
- if (v.phase !== 'rating') {
298
- return (
299
- <Box flexDirection="column">
300
- {v.skipped && <Text dimColor>{v.skipped}</Text>}
301
- <Text>{v.message ?? ''}</Text>
302
- <Button key="close" label="Close" hotkey="c" role="dismiss" onPress={close} />
303
- </Box>
304
- )
305
- }
306
-
307
- const idle = !v.busy
308
- const buttons = (
309
- <Box flexDirection="row" gap={2}>
310
- <Button key="ok" label="OK" hotkey="1" variant="primary" dimColor={!idle}
311
- onPress={() => void rate($, ['--rating', '1'])} />
312
- <Button key="weak" label="Weak" hotkey="2" dimColor={!idle}
313
- onPress={() => void rate($, ['--rating', '0'])} />
314
- <Button key="research" label="Research" hotkey="3" dimColor={!idle}
315
- onPress={() => void research($)} />
316
- <Button key="stop" label="Stop" hotkey="4" role="dismiss" onPress={close} />
317
- </Box>
318
- )
319
-
320
- let field = null
321
- if (e.surface !== 'mobile') {
322
- const { Input } = $.ui.resolve(e)
323
- field = (
324
- <Input key="comment" label="Comment" submitLabel="save rule"
325
- placeholder="becomes a rule for this channel · ?question = research · stop"
326
- onSubmit={value => void comment($, value)} />
327
- )
328
- }
329
-
330
- return (
331
- <Box flexDirection="column" gap={1}>
332
- {v.skipped && <Text dimColor>{v.skipped}</Text>}
333
- <Markdown text={(v.summary ?? '').slice(0, 10000)} />
334
- {v.busy ? <Text dimColor>{v.busy}</Text> : buttons}
335
- {field}
336
- <Text dimColor>OK = neutral · Weak = teach the filter to skip titles like this · Research = dig into it with Claude</Text>
337
- </Box>
338
- )
339
- })
340
186
  }
@@ -5,24 +5,3 @@ export type Pending = {
5
5
  type: string
6
6
  publishedAt: string
7
7
  }
8
-
9
- export type View = {
10
- phase: 'idle' | 'loading' | 'rating' | 'done' | 'error'
11
- /** Briefing markdown of the video awaiting a rating. */
12
- summary?: string
13
- pending?: Pending
14
- /** Language the engine writes in; the research hand-off asks for it too. */
15
- lang?: string
16
- /** Videos the filters dropped on the way to this one, one line. */
17
- skipped?: string
18
- /** Error or status text for the done/error phases. */
19
- message?: string
20
- /** Set while a rating or comment is being written; presses are ignored meanwhile. */
21
- busy?: string
22
- }
23
-
24
- declare module 'claude-code' {
25
- interface PluginState {
26
- 'yt-briefing': { view: View }
27
- }
28
- }