throughline 0.3.18 → 0.3.19

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": "throughline",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -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
+ });