throughline 0.3.6 → 0.3.8

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.6",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -235,21 +235,27 @@ export function shouldForceFullRedraw(prevCols, currCols) {
235
235
  * 描画に使う列幅を解決する。
236
236
  *
237
237
  * 優先順:
238
- * 1. `process.stdout.columns` が 40 以上 → その値から 1 引いたもの (末尾列での自動改行回避)
239
- * 2. `process.env.COLUMNS` 40 以上 → その値
240
- * 3. それ以外 200 にフォールバック(VSCode `type: process` タスクで stdout が
241
- * TTY でない場合、columns undefined または 0/12 のような極端に小さい値を
242
- * 返すことがあり、そのまま使うと描画が「openclaw」で切れるなどの視覚崩れが出る)
238
+ * 1. **stdout が TTY** かつ `process.stdout.columns` が 40 以上
239
+ * その値から 1 引いたもの(末尾列での自動改行回避)
240
+ * 2. `process.env.COLUMNS` 40 以上 その値 - 1
241
+ * 3. それ以外 200 にフォールバック
243
242
  *
244
- * 「40 未満は無視」閾値は、現行 formatLine の最小構成(marker + project(18) + id(8) +
245
- * ago(8) + bar(20) + ...)が実用上 40 セル未満になり得ないため。
243
+ * TTY のとき columns を信用しない理由:
244
+ * VSCode `type: process` タスクは stdout PTY ではなくパイプとして渡すため、
245
+ * - 起動時の columns が undefined / 0 / 12 のような極端な値になることがある
246
+ * - ターミナル panel をドラッグで広げても SIGWINCH が届かず columns が更新されない
247
+ * - 結果として起動時に狭かった幅のまま永久に truncate され続ける
248
+ * `isTTY` が false の時点で columns 値は信頼できない契約だと割り切り、env.COLUMNS か
249
+ * 固定 200 にフォールバックする。
246
250
  *
247
- * 固定 200 フォールバックは、truncateToCells が 200 セルより短い実内容を
248
- * そのまま通す (伸長しない) ので過大でも副作用なし。
251
+ * 200 固定フォールバックは、truncateToCells が 200 セル以下の実内容をそのまま通す
252
+ * (伸長しない)ので過大でも副作用なし。
249
253
  */
250
254
  export function resolveColumns() {
251
- const reported = typeof process.stdout.columns === 'number' ? process.stdout.columns : 0;
252
- if (reported >= 40) return reported - 1;
255
+ if (process.stdout.isTTY) {
256
+ const reported = typeof process.stdout.columns === 'number' ? process.stdout.columns : 0;
257
+ if (reported >= 40) return reported - 1;
258
+ }
253
259
  const fromEnv = Number(process.env.COLUMNS);
254
260
  if (Number.isFinite(fromEnv) && fromEnv >= 40) return fromEnv - 1;
255
261
  return 200;
@@ -414,13 +420,20 @@ function renderFrame(args) {
414
420
  const columns = resolveColumns();
415
421
  const clipped = lines.map((l) => truncateToCells(l, columns));
416
422
 
417
- // 前フレームを消去してから再描画:
418
- // 1. カーソルを前フレームの先頭行へ戻す (CUU = 行移動のみ)
419
- // 2. 1 へ戻す (CR)
420
- // 3. 現在位置から画面末尾までを一括消去 (ED 0)
421
- // CPL (\x1b[nF) は VSCode 統合ターミナルで挙動が不安定だったため使わない
422
- if (lastRenderedLines > 0) {
423
- process.stdout.write(ANSI.up(lastRenderedLines) + '\r' + ANSI.clearBelow);
423
+ // 再描画戦略:
424
+ // - TTY: 真の columns が分かるので CUU + clearBelow で部分再描画(省フリッカ)
425
+ // - TTY (VSCode の type:process タスク等): columns を信用できないので 200 で
426
+ // truncate しているが、実ターミナル幅が 200 未満なら自動改行が起き、論理行数と
427
+ // 物理行数がズレて CUU が誤作動する(「1 セッション」行が毎フレーム積み上がる
428
+ // バグを実機で確認)。非 TTY では画面全クリアで愚直に描き直す方が確実。
429
+ // どちらも差分検知 (needsRerender) を通過したフレームのみ書き込むので、
430
+ // フリッカ量はデータ変化頻度に比例するだけで爆発しない。
431
+ if (process.stdout.isTTY) {
432
+ if (lastRenderedLines > 0) {
433
+ process.stdout.write(ANSI.up(lastRenderedLines) + '\r' + ANSI.clearBelow);
434
+ }
435
+ } else {
436
+ process.stdout.write(ANSI.clearScreen);
424
437
  }
425
438
 
426
439
  process.stdout.write(clipped.join('\n') + '\n');
@@ -388,54 +388,60 @@ test('shouldForceFullRedraw: 片方 0 以下は false (未初期化 or 無効)',
388
388
 
389
389
  // ─── resolveColumns ───────────────────────────────────────────────
390
390
 
391
- test('resolveColumns: process.stdout.columns 40 以上ならそれ - 1 を返す', () => {
392
- const orig = process.stdout.columns;
391
+ function withStdoutState({ isTTY, columns, envColumns }, fn) {
392
+ const origIsTTY = process.stdout.isTTY;
393
+ const origColumns = process.stdout.columns;
394
+ const origEnv = process.env.COLUMNS;
393
395
  try {
394
- Object.defineProperty(process.stdout, 'columns', { value: 120, configurable: true, writable: true });
395
- assert.equal(resolveColumns(), 119);
396
+ Object.defineProperty(process.stdout, 'isTTY', { value: isTTY, configurable: true, writable: true });
397
+ Object.defineProperty(process.stdout, 'columns', { value: columns, configurable: true, writable: true });
398
+ if (envColumns === undefined) delete process.env.COLUMNS;
399
+ else process.env.COLUMNS = envColumns;
400
+ return fn();
396
401
  } finally {
397
- Object.defineProperty(process.stdout, 'columns', { value: orig, configurable: true, writable: true });
402
+ Object.defineProperty(process.stdout, 'isTTY', { value: origIsTTY, configurable: true, writable: true });
403
+ Object.defineProperty(process.stdout, 'columns', { value: origColumns, configurable: true, writable: true });
404
+ if (origEnv === undefined) delete process.env.COLUMNS;
405
+ else process.env.COLUMNS = origEnv;
398
406
  }
407
+ }
408
+
409
+ test('resolveColumns: TTY かつ columns が 40 以上 → その値 - 1', () => {
410
+ withStdoutState({ isTTY: true, columns: 120, envColumns: undefined }, () => {
411
+ assert.equal(resolveColumns(), 119);
412
+ });
399
413
  });
400
414
 
401
- test('resolveColumns: columns が小さすぎる値 (12 等) ならフォールバック 200', () => {
402
- const orig = process.stdout.columns;
403
- const origEnv = process.env.COLUMNS;
404
- try {
405
- Object.defineProperty(process.stdout, 'columns', { value: 12, configurable: true, writable: true });
406
- delete process.env.COLUMNS;
415
+ test('resolveColumns: 非 TTY なら columns が大きくても信用しない (env 200 も使う)', () => {
416
+ // type: process タスクで columns が 120 にセットされたが実際の幅とは連動しない、という状況。
417
+ // env.COLUMNS も無ければ 200 フォールバック。
418
+ withStdoutState({ isTTY: false, columns: 120, envColumns: undefined }, () => {
407
419
  assert.equal(resolveColumns(), 200);
408
- } finally {
409
- Object.defineProperty(process.stdout, 'columns', { value: orig, configurable: true, writable: true });
410
- if (origEnv !== undefined) process.env.COLUMNS = origEnv;
411
- }
420
+ });
412
421
  });
413
422
 
414
- test('resolveColumns: columns undefined でも env.COLUMNS >= 40 があればそれを使う', () => {
415
- const orig = process.stdout.columns;
416
- const origEnv = process.env.COLUMNS;
417
- try {
418
- Object.defineProperty(process.stdout, 'columns', { value: undefined, configurable: true, writable: true });
419
- process.env.COLUMNS = '150';
423
+ test('resolveColumns: TTY でも columns が小さすぎる値 (12 等) ならフォールバック 200', () => {
424
+ withStdoutState({ isTTY: true, columns: 12, envColumns: undefined }, () => {
425
+ assert.equal(resolveColumns(), 200);
426
+ });
427
+ });
428
+
429
+ test('resolveColumns: 非 TTY でも env.COLUMNS >= 40 があればそれを使う', () => {
430
+ withStdoutState({ isTTY: false, columns: undefined, envColumns: '150' }, () => {
420
431
  assert.equal(resolveColumns(), 149);
421
- } finally {
422
- Object.defineProperty(process.stdout, 'columns', { value: orig, configurable: true, writable: true });
423
- if (origEnv === undefined) delete process.env.COLUMNS;
424
- else process.env.COLUMNS = origEnv;
425
- }
432
+ });
433
+ });
434
+
435
+ test('resolveColumns: TTY で columns 未設定、env も無ければフォールバック 200', () => {
436
+ withStdoutState({ isTTY: true, columns: undefined, envColumns: undefined }, () => {
437
+ assert.equal(resolveColumns(), 200);
438
+ });
426
439
  });
427
440
 
428
441
  test('resolveColumns: 全てのソースが無効ならフォールバック 200', () => {
429
- const orig = process.stdout.columns;
430
- const origEnv = process.env.COLUMNS;
431
- try {
432
- Object.defineProperty(process.stdout, 'columns', { value: undefined, configurable: true, writable: true });
433
- delete process.env.COLUMNS;
442
+ withStdoutState({ isTTY: false, columns: undefined, envColumns: undefined }, () => {
434
443
  assert.equal(resolveColumns(), 200);
435
- } finally {
436
- Object.defineProperty(process.stdout, 'columns', { value: orig, configurable: true, writable: true });
437
- if (origEnv !== undefined) process.env.COLUMNS = origEnv;
438
- }
444
+ });
439
445
  });
440
446
 
441
447
  // ─── formatLine: time-ago 表示 ────────────────────────────────────