throughline 0.3.10 → 0.3.11

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.10",
3
+ "version": "0.3.11",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -34,7 +34,11 @@ const ANSI = {
34
34
  hideCursor: '\x1b[?25l',
35
35
  showCursor: '\x1b[?25h',
36
36
  clearLine: '\x1b[2K',
37
- clearScreen: '\x1b[2J\x1b[H',
37
+ // xterm.js は `\x1b[2J` (ED2) をビューポート消去としてしか実装しておらず、
38
+ // 消えた内容はスクロールバックに残る(xterm.js issue #5019 の jerch コメント参照)。
39
+ // VSCode task panel でフレーム更新が「積み上がる」ように見えるのはこれが原因。
40
+ // `\x1b[3J` (ED3) を続けて送るとスクロールバックも消えて真の全クリアになる。
41
+ clearScreen: '\x1b[2J\x1b[3J\x1b[H',
38
42
  clearBelow: '\x1b[0J', // 現在位置から画面末尾までをクリア
39
43
  up: (n) => `\x1b[${n}A`, // CUU: カーソルを N 行上へ (列は変えない)
40
44
  reset: '\x1b[0m',
@@ -162,14 +166,16 @@ function padCellsEnd(s, targetCells) {
162
166
  // --- CLI 引数 ---
163
167
  /**
164
168
  * @param {string[]} argv
165
- * @returns {{all: boolean, session: string|null}}
169
+ * @returns {{all: boolean, session: string|null, diag: boolean}}
166
170
  * @throws {Error} --session に値が欠落している場合
167
171
  */
168
172
  function parseArgs(argv) {
169
- const args = { all: false, session: null };
173
+ const args = { all: false, session: null, diag: false };
170
174
  for (let i = 0; i < argv.length; i++) {
171
175
  if (argv[i] === '--all') {
172
176
  args.all = true;
177
+ } else if (argv[i] === '--diag') {
178
+ args.diag = true;
173
179
  } else if (argv[i] === '--session') {
174
180
  const value = argv[i + 1];
175
181
  if (value === undefined || value.startsWith('--')) {
@@ -462,6 +468,70 @@ function safeRenderFrame(args) {
462
468
  }
463
469
  }
464
470
 
471
+ /**
472
+ * 環境診断モード。ユーザー環境で「モニターの描画が壊れる」と報告されたときに、
473
+ * 実際の process.stdout / env / TERM の値をその場で可視化するための一発起動モード。
474
+ *
475
+ * 過去に「`type: process` だと非 TTY」「`type: shell` なら PTY」など推測で描画戦略を
476
+ * 変えてきたが、PTY が張られるかどうかは VSCode のバージョンや Windows の ConPTY
477
+ * 実装に依存し、推測は外れ続けた。このコマンドで実測値を 1 ページに出すことで、
478
+ * 「この環境では何が起きているか」を断定できるようにする。
479
+ */
480
+ function runDiagnostic() {
481
+ const out = (k, v) => process.stdout.write(`${k.padEnd(28)}${v}\n`);
482
+ process.stdout.write('=== Throughline monitor diagnostic ===\n\n');
483
+
484
+ process.stdout.write('[process.stdout]\n');
485
+ out(' isTTY', String(Boolean(process.stdout.isTTY)));
486
+ out(' columns', String(process.stdout.columns ?? '(undefined)'));
487
+ out(' rows', String(process.stdout.rows ?? '(undefined)'));
488
+ out(' hasColors()',
489
+ typeof process.stdout.hasColors === 'function'
490
+ ? String(process.stdout.hasColors())
491
+ : '(n/a)',
492
+ );
493
+ process.stdout.write('\n');
494
+
495
+ process.stdout.write('[process.stderr]\n');
496
+ out(' isTTY', String(Boolean(process.stderr.isTTY)));
497
+ process.stdout.write('\n');
498
+
499
+ process.stdout.write('[env]\n');
500
+ for (const key of ['TERM', 'TERM_PROGRAM', 'TERM_PROGRAM_VERSION', 'COLUMNS', 'LINES', 'VSCODE_PID', 'VSCODE_IPC_HOOK_CLI', 'VSCODE_INJECTION', 'WT_SESSION', 'ConEmuPID']) {
501
+ out(` ${key}`, process.env[key] ?? '(unset)');
502
+ }
503
+ process.stdout.write('\n');
504
+
505
+ process.stdout.write('[resolveColumns()]\n');
506
+ out(' value', String(resolveColumns()));
507
+ process.stdout.write('\n');
508
+
509
+ // ANSI 検証: 画面クリア系シーケンスが視覚的にどう動くかを判定する
510
+ // 小テスト。ユーザーには生出力を見て「積み上がっているか」報告してもらう。
511
+ process.stdout.write('[ANSI probe — 視認用]\n');
512
+ process.stdout.write(' 直後に 3 回フレームを書きます。各フレームは clearScreen で上書きされるはず。\n');
513
+ process.stdout.write(' スクリーンショットを取って、フレーム A/B/C が「積み上がり」か「上書き」か教えてください。\n\n');
514
+
515
+ let frame = 0;
516
+ const labels = ['A', 'B', 'C'];
517
+ const probe = () => {
518
+ process.stdout.write(ANSI.clearScreen);
519
+ process.stdout.write(`=== frame ${labels[frame]} / 3 ===\n`);
520
+ process.stdout.write('この行より上に「=== frame X ===」が 2 つ以上見える場合、\n');
521
+ process.stdout.write(`\\x1b[2J\\x1b[3J\\x1b[H (現行の clearScreen) がこの端末で効いていません。\n`);
522
+ process.stdout.write(`現行 clearScreen = 0x1b[2J 0x1b[3J 0x1b[H\n`);
523
+ frame++;
524
+ if (frame >= labels.length) {
525
+ process.stdout.write('\n=== diag 終了 ===\n');
526
+ process.stdout.write('上記の値と、この 3 フレームが積み上がったかどうかを報告してください。\n');
527
+ process.exit(0);
528
+ } else {
529
+ setTimeout(probe, 1500);
530
+ }
531
+ };
532
+ setTimeout(probe, 500);
533
+ }
534
+
465
535
  export function main() {
466
536
  let args;
467
537
  try {
@@ -472,6 +542,11 @@ export function main() {
472
542
  process.exit(2);
473
543
  }
474
544
 
545
+ if (args.diag) {
546
+ runDiagnostic();
547
+ return;
548
+ }
549
+
475
550
  process.stdout.write(ANSI.hideCursor);
476
551
  process.stdout.write(color(ANSI.dim, `[Throughline] モニター起動 (state: ${getStateDir()}, Ctrl+C で終了)\n`));
477
552
 
@@ -26,19 +26,23 @@ const CWD_BAR = normalizeProjectPath('/tmp/bar');
26
26
  // ─── parseArgs ─────────────────────────────────────────────────────
27
27
 
28
28
  test('parseArgs: 引数なしは defaults', () => {
29
- assert.deepEqual(parseArgs([]), { all: false, session: null });
29
+ assert.deepEqual(parseArgs([]), { all: false, session: null, diag: false });
30
30
  });
31
31
 
32
32
  test('parseArgs: --all フラグ', () => {
33
- assert.deepEqual(parseArgs(['--all']), { all: true, session: null });
33
+ assert.deepEqual(parseArgs(['--all']), { all: true, session: null, diag: false });
34
34
  });
35
35
 
36
36
  test('parseArgs: --session <id>', () => {
37
- assert.deepEqual(parseArgs(['--session', 'abc123']), { all: false, session: 'abc123' });
37
+ assert.deepEqual(parseArgs(['--session', 'abc123']), { all: false, session: 'abc123', diag: false });
38
+ });
39
+
40
+ test('parseArgs: --diag フラグ (環境診断モード)', () => {
41
+ assert.deepEqual(parseArgs(['--diag']), { all: false, session: null, diag: true });
38
42
  });
39
43
 
40
44
  test('parseArgs: --all と --session の組み合わせ', () => {
41
- assert.deepEqual(parseArgs(['--all', '--session', 'abc']), { all: true, session: 'abc' });
45
+ assert.deepEqual(parseArgs(['--all', '--session', 'abc']), { all: true, session: 'abc', diag: false });
42
46
  });
43
47
 
44
48
  test('parseArgs: --session 値欠落は throw する', () => {
@@ -51,7 +55,7 @@ test('parseArgs: --session の次が別フラグなら throw する', () => {
51
55
 
52
56
  test('parseArgs: 未知の引数は黙殺', () => {
53
57
  // 将来 --help などを足す余地を残すため、現状は黙殺で OK
54
- assert.deepEqual(parseArgs(['--unknown', 'value']), { all: false, session: null });
58
+ assert.deepEqual(parseArgs(['--unknown', 'value']), { all: false, session: null, diag: false });
55
59
  });
56
60
 
57
61
  // ─── filterStates ─────────────────────────────────────────────────