throughline 0.3.18 → 0.3.20

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
@@ -205,24 +205,31 @@ Example output (real values from a running 1M-context Opus session):
205
205
  ### VS Code auto-start (automatic)
206
206
 
207
207
  After `throughline install`, any VS Code / Cursor / VSCodium project you work in
208
- gets `.vscode/tasks.json` provisioned automatically on the next assistant turn.
208
+ gets `.vscode/tasks.json` provisioned automatically on the first session event.
209
209
  The file configures `runOn: folderOpen` so the monitor appears in a dedicated
210
210
  terminal panel the next time you open that folder.
211
211
 
212
- **How it works.** The Stop hook runs at the end of every assistant response.
212
+ **How it works.** `ensureMonitorTaskFile` is called from **all three hooks
213
+ (SessionStart, UserPromptSubmit, Stop)** as of v0.3.18. Whichever one fires
214
+ first in your environment creates the file; the rest are idempotent no-ops.
213
215
  Once per project it inspects `.vscode/tasks.json`:
214
216
 
215
- - **No file yet** → creates one with a single `Throughline Monitor` task.
217
+ - **No file yet** → creates one with a single `Throughline Monitor` task, and
218
+ emits a one-time `<system-reminder>` to stdout so Claude tells you a
219
+ **Developer: Reload Window** is needed to activate the `folderOpen` task once
220
+ (v0.3.19+).
216
221
  - **Plain JSON with other tasks** → appends the monitor task, preserves your
217
- existing entries, `version`, and indentation.
222
+ existing entries, `version`, and indentation (same notice fires once).
218
223
  - **JSONC (comments or trailing commas)** → does not touch the file. Prints a
219
224
  one-time notice to stderr asking you to paste the snippet below.
220
225
  - **Already contains a Throughline Monitor task** → does nothing (idempotent;
221
- this is the common path on every subsequent turn).
226
+ this is the common path on every subsequent turn; notice is silent).
222
227
 
223
- The generated task uses `type: 'process'` with the absolute path to Node and
224
- `bin/throughline.mjs` so Windows `.cmd` shims and missing PATH entries cannot
225
- break it.
228
+ The generated task uses `type: 'shell'` with the absolute path to Node and
229
+ `bin/throughline.mjs`. VS Code wraps shell tasks in a PTY (xterm.js) so the
230
+ monitor sees `isTTY=true`, real `columns`, and resize events. Windows `.cmd`
231
+ shims and missing PATH entries cannot break it because the command is already
232
+ an absolute Node binary path.
226
233
 
227
234
  **Opt out:** set `THROUGHLINE_NO_VSCODE=1` in the environment used by Claude
228
235
  Code. Delete `.vscode/tasks.json` (or just the monitor entry) if you want to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -304,8 +304,8 @@ function formatLine({ state, usage, isActive, now = Date.now() }) {
304
304
  // 90%超: !! + 強い文言 (赤)
305
305
  // 70%超: ! + 弱い文言 (黄)
306
306
  const warn =
307
- ratio >= 0.9 ? color(ANSI.red + ANSI.bold, ' !! /clear 強く推奨') :
308
- ratio >= 0.7 ? color(ANSI.yellow, ' ! そろそろ /clear') :
307
+ ratio >= 0.9 ? color(ANSI.red + ANSI.bold, ' !! /tl 強く推奨') :
308
+ ratio >= 0.7 ? color(ANSI.yellow, ' ! そろそろ /tl') :
309
309
  '';
310
310
 
311
311
  const marker = isActive ? color(ANSI.bold + ANSI.cyan, '▶') : ' ';
@@ -305,13 +305,13 @@ test('formatLine: 70% 未満は警告テキストなし', () => {
305
305
  const out = stripColors(formatLine(makeLineArgs(0.5)));
306
306
  assert.ok(!out.includes('!!'));
307
307
  assert.ok(!out.includes('! '));
308
- assert.ok(!out.includes('/clear'));
308
+ assert.ok(!out.includes('/tl'));
309
309
  });
310
310
 
311
311
  test('formatLine: 70% 以上で "!" マーカーと弱めの文言', () => {
312
312
  const out = stripColors(formatLine(makeLineArgs(0.75)));
313
313
  assert.ok(out.includes('!'), 'should include ! marker');
314
- assert.ok(out.includes('そろそろ /clear'), 'should show soft warning');
314
+ assert.ok(out.includes('そろそろ /tl'), 'should show soft warning');
315
315
  assert.ok(!out.includes('!!'), 'should not include critical marker yet');
316
316
  });
317
317
 
@@ -161,6 +161,37 @@ function atomicWrite(targetPath, contents) {
161
161
  renameSync(tmp, targetPath);
162
162
  }
163
163
 
164
+ /**
165
+ * tasks.json を「今まさに作成/マージした」瞬間にユーザー向けへ送る通知テキスト。
166
+ *
167
+ * SessionStart / UserPromptSubmit の stdout は Claude Code に additional context として
168
+ * 取り込まれる。<system-reminder> で包んで Claude に「これをユーザーに伝える」ことを
169
+ * 明示しておくことで、新規プロジェクト初回セットアップ直後にユーザーへ
170
+ * 「Reload Window が必要」を即時通知できる。
171
+ *
172
+ * 通知するのは action が 'created' か 'merged' の場合のみ。既に存在した場合は
173
+ * セットアップ済みのため沈黙する(冪等性)。
174
+ */
175
+ export function buildSetupNotice(action) {
176
+ if (action !== 'created' && action !== 'merged') return null;
177
+ return (
178
+ '<system-reminder>\n' +
179
+ 'Throughline: このプロジェクトに token-monitor の自動起動設定を追加しました (.vscode/tasks.json)。\n' +
180
+ '今すぐ monitor を起動するには、VSCode のコマンドパレット (Ctrl+Shift+P) から\n' +
181
+ ' Developer: Reload Window\n' +
182
+ 'を実行してください。次回以降はフォルダを開いた瞬間に自動起動します。\n' +
183
+ '\n' +
184
+ 'このお知らせは今このプロジェクトで初回セットアップが完了したためだけに 1 度だけ流れています。\n' +
185
+ '必ずユーザー(あなたの対話相手)に日本語でそのまま伝えてください。\n' +
186
+ '</system-reminder>\n'
187
+ );
188
+ }
189
+
190
+ function emitSetupNotice(action) {
191
+ const text = buildSetupNotice(action);
192
+ if (text) process.stdout.write(text);
193
+ }
194
+
164
195
  function emitJsoncGuidanceOnce(vscodeDir) {
165
196
  const markerPath = join(vscodeDir, JSONC_MARKER_FILENAME);
166
197
  if (existsSync(markerPath)) return;
@@ -215,6 +246,7 @@ export function ensureMonitorTaskFile(opts = {}) {
215
246
  if (!existsSync(vscodeDir)) mkdirSync(vscodeDir, { recursive: true });
216
247
  const obj = { version: '2.0.0', tasks: [buildMonitorTask(bin)] };
217
248
  atomicWrite(tasksPath, JSON.stringify(obj, null, 2) + '\n');
249
+ emitSetupNotice('created');
218
250
  return { action: 'created', path: tasksPath };
219
251
  }
220
252
 
@@ -243,5 +275,6 @@ export function ensureMonitorTaskFile(opts = {}) {
243
275
  tasks: [...(Array.isArray(obj.tasks) ? obj.tasks : []), buildMonitorTask(bin)],
244
276
  };
245
277
  atomicWrite(tasksPath, JSON.stringify(nextObj, null, indent) + '\n');
278
+ emitSetupNotice('merged');
246
279
  return { action: 'merged', path: tasksPath };
247
280
  }
@@ -10,6 +10,7 @@ import {
10
10
  detectIndent,
11
11
  hasMonitorTask,
12
12
  buildMonitorTask,
13
+ buildSetupNotice,
13
14
  } from './vscode-task.mjs';
14
15
 
15
16
  const VSCODE_ENV = { TERM_PROGRAM: 'vscode' };
@@ -518,3 +519,60 @@ test('ensureMonitorTaskFile: parse_error for malformed JSON', () => {
518
519
  cleanup();
519
520
  }
520
521
  });
522
+
523
+ // --- buildSetupNotice ---
524
+
525
+ test('buildSetupNotice: returns notice text for created', () => {
526
+ const text = buildSetupNotice('created');
527
+ assert.ok(text && text.includes('<system-reminder>'));
528
+ assert.ok(text.includes('Reload Window'));
529
+ assert.ok(text.includes('tasks.json'));
530
+ assert.ok(text.includes('ユーザー'));
531
+ });
532
+
533
+ test('buildSetupNotice: returns notice text for merged', () => {
534
+ const text = buildSetupNotice('merged');
535
+ assert.ok(text && text.includes('<system-reminder>'));
536
+ assert.ok(text.includes('Reload Window'));
537
+ });
538
+
539
+ test('buildSetupNotice: returns null for already_present (silent idempotency)', () => {
540
+ assert.equal(buildSetupNotice('already_present'), null);
541
+ });
542
+
543
+ test('buildSetupNotice: returns null for skipped', () => {
544
+ assert.equal(buildSetupNotice('skipped'), null);
545
+ });
546
+
547
+ test('buildSetupNotice: ensureMonitorTaskFile writes notice to stdout on first creation', () => {
548
+ const { dir, cleanup } = mkTmpCwd();
549
+ const captured = [];
550
+ const origWrite = process.stdout.write.bind(process.stdout);
551
+ process.stdout.write = (chunk) => {
552
+ captured.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
553
+ return true;
554
+ };
555
+ try {
556
+ const r1 = ensureMonitorTaskFile({
557
+ cwd: dir,
558
+ env: VSCODE_ENV,
559
+ throughlineBin: FAKE_BIN,
560
+ });
561
+ assert.equal(r1.action, 'created');
562
+ const r2 = ensureMonitorTaskFile({
563
+ cwd: dir,
564
+ env: VSCODE_ENV,
565
+ throughlineBin: FAKE_BIN,
566
+ });
567
+ assert.equal(r2.action, 'already_present');
568
+ } finally {
569
+ process.stdout.write = origWrite;
570
+ cleanup();
571
+ }
572
+ const joined = captured.join('');
573
+ assert.ok(joined.includes('<system-reminder>'), 'notice should be written on created');
574
+ assert.ok(joined.includes('Reload Window'));
575
+ // 2 回目 (already_present) では notice は出ない = created 分の 1 回のみ
576
+ const count = (joined.match(/<system-reminder>/g) ?? []).length;
577
+ assert.equal(count, 1, 'notice should be emitted exactly once (idempotency)');
578
+ });