throughline 0.3.4 → 0.3.6
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 +25 -10
- package/package.json +1 -1
- package/src/token-monitor.mjs +33 -6
- package/src/token-monitor.test.mjs +73 -2
- package/src/vscode-task.mjs +9 -0
- package/src/vscode-task.test.mjs +7 -0
package/README.md
CHANGED
|
@@ -181,11 +181,14 @@ Example output (real values from a running 1M-context Opus session):
|
|
|
181
181
|
- **Resize resilient.** Column width is polled every second, so pane drags that
|
|
182
182
|
don't fire a terminal `resize` event (common in VS Code's integrated
|
|
183
183
|
terminal) still trigger a full redraw.
|
|
184
|
-
- **Per-row "last updated" stamp.** Each session row
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
- **Per-row "last updated" stamp.** Each session row carries an 8-cell
|
|
185
|
+
`just now` / `24m ago` stamp right after the session id, placed before the
|
|
186
|
+
bar so narrow terminals don't truncate it. It resets to `just now` on every
|
|
187
|
+
Stop hook, so a growing stamp means the session is truly idle — not the
|
|
188
|
+
monitor stuck. When you need more detail,
|
|
189
|
+
`throughline doctor --session <id-prefix>` compares the state file against
|
|
190
|
+
the actual transcript JSONL and flags drift, idle time, and
|
|
191
|
+
`/clear`-induced transcript path staleness.
|
|
189
192
|
- **State-backed usage snapshot.** When the Stop hook finishes a turn it
|
|
190
193
|
persists the latest `tokens / model / contextWindowSize` back into the state
|
|
191
194
|
file. The monitor prefers this snapshot over re-reading the JSONL, which
|
|
@@ -379,6 +382,13 @@ No session has touched its state file in the last 15 minutes. Send a message in
|
|
|
379
382
|
Claude Code and the monitor should pick it up within 1 second. If it still does
|
|
380
383
|
not, run `throughline doctor`.
|
|
381
384
|
|
|
385
|
+
**Monitor seems stuck on the same value**
|
|
386
|
+
Each session row ends with a `(Nm ago)` stamp. If it keeps growing, the session
|
|
387
|
+
is idle — no assistant turn has finished. For a deeper look, run
|
|
388
|
+
`throughline doctor --session <id-prefix>` to compare the state file against
|
|
389
|
+
the actual transcript JSONL and flag drift, idle time, or `/clear`-induced
|
|
390
|
+
transcript path staleness.
|
|
391
|
+
|
|
382
392
|
**`throughline install` wrote to the wrong settings file**
|
|
383
393
|
By default, Throughline installs to `~/.claude/settings.json` (user scope, applies
|
|
384
394
|
to all projects). Use `--project` to scope it to the current directory's
|
|
@@ -430,13 +440,18 @@ the folder in VS Code.
|
|
|
430
440
|
|
|
431
441
|
## Design docs
|
|
432
442
|
|
|
433
|
-
- [`docs/L1_L2_L3_REDESIGN.md`](docs/L1_L2_L3_REDESIGN.md) — **
|
|
443
|
+
- [`docs/L1_L2_L3_REDESIGN.md`](docs/L1_L2_L3_REDESIGN.md) — **core design
|
|
434
444
|
spec** for the L1/L2/L3 differential layer model (schema v4 base + v5 L3
|
|
435
|
-
classification extension). Authoritative.
|
|
436
|
-
- [`docs/
|
|
437
|
-
|
|
445
|
+
classification extension). Authoritative for the memory layering rules.
|
|
446
|
+
- [`docs/INHERITANCE_ON_CLEAR_ONLY.md`](docs/INHERITANCE_ON_CLEAR_ONLY.md) —
|
|
447
|
+
design record for the `/tl` baton handoff system (schema v6–v7). Explains
|
|
448
|
+
why the current inheritance is opt-in rather than heuristic.
|
|
449
|
+
- [`docs/PUBLIC_RELEASE_PLAN.md`](docs/PUBLIC_RELEASE_PLAN.md) — public
|
|
450
|
+
release plan, implementation status by version, § 0 fallback rule, and
|
|
451
|
+
remaining tasks.
|
|
438
452
|
- [`docs/archive/`](docs/archive/) — superseded design documents kept for
|
|
439
|
-
historical reference (original CONCEPT, session-linking experiments,
|
|
453
|
+
historical reference (original CONCEPT, session-linking experiments,
|
|
454
|
+
pre-publish action list).
|
|
440
455
|
|
|
441
456
|
---
|
|
442
457
|
|
package/package.json
CHANGED
package/src/token-monitor.mjs
CHANGED
|
@@ -231,6 +231,30 @@ export function shouldForceFullRedraw(prevCols, currCols) {
|
|
|
231
231
|
return prevCols !== currCols && currCols > 0;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* 描画に使う列幅を解決する。
|
|
236
|
+
*
|
|
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」で切れるなどの視覚崩れが出る)
|
|
243
|
+
*
|
|
244
|
+
* 「40 未満は無視」閾値は、現行 formatLine の最小構成(marker + project(18) + id(8) +
|
|
245
|
+
* ago(8) + bar(20) + ...)が実用上 40 セル未満になり得ないため。
|
|
246
|
+
*
|
|
247
|
+
* 固定 200 フォールバックは、truncateToCells が 200 セルより短い実内容を
|
|
248
|
+
* そのまま通す (伸長しない) ので過大でも副作用なし。
|
|
249
|
+
*/
|
|
250
|
+
export function resolveColumns() {
|
|
251
|
+
const reported = typeof process.stdout.columns === 'number' ? process.stdout.columns : 0;
|
|
252
|
+
if (reported >= 40) return reported - 1;
|
|
253
|
+
const fromEnv = Number(process.env.COLUMNS);
|
|
254
|
+
if (Number.isFinite(fromEnv) && fromEnv >= 40) return fromEnv - 1;
|
|
255
|
+
return 200;
|
|
256
|
+
}
|
|
257
|
+
|
|
234
258
|
function formatLine({ state, usage, isActive, now = Date.now() }) {
|
|
235
259
|
const project = basename(state.projectPath || '?');
|
|
236
260
|
const shortId = state.sessionId.slice(0, 8);
|
|
@@ -263,11 +287,15 @@ function formatLine({ state, usage, isActive, now = Date.now() }) {
|
|
|
263
287
|
const modelCol = usage?.model ? color(ANSI.dim, usage.model) : color(ANSI.dim, '(未取得)');
|
|
264
288
|
// 最終更新からの経過: 表示が「止まって見える」とき、それが idle なのか障害なのかを
|
|
265
289
|
// 即座に判別できるようにする。updatedAt は state.writeSessionState 時の Date.now()。
|
|
266
|
-
|
|
267
|
-
|
|
290
|
+
// 位置は session id の直後(左寄せ固定幅)。狭いターミナルでもモデル名より先に
|
|
291
|
+
// 切れない位置に置くのと、縦の視線移動で「どれが最新更新か」を把握しやすくするため。
|
|
292
|
+
const agoText = typeof state.updatedAt === 'number'
|
|
293
|
+
? formatTimeAgo(now - state.updatedAt)
|
|
268
294
|
: '';
|
|
295
|
+
// 8 セル固定: "just now" が最長 (8 セル)、"99d ago" は 7 セル。括弧なしで OK
|
|
296
|
+
const agoCol = color(ANSI.dim, padCellsEnd(agoText, 8));
|
|
269
297
|
|
|
270
|
-
return `${marker} ${projectCol} ${idCol} ${barCol} ${tokCol} ${remCol} ${modelCol}
|
|
298
|
+
return `${marker} ${projectCol} ${idCol} ${agoCol} ${barCol} ${tokCol} ${remCol} ${modelCol}${warn}`;
|
|
271
299
|
}
|
|
272
300
|
|
|
273
301
|
// --- フィルタ ---
|
|
@@ -383,9 +411,7 @@ function renderFrame(args) {
|
|
|
383
411
|
// こうすれば ANSI.up(lines.length) と論理行数が物理行数と一致する。
|
|
384
412
|
// columns - 1 にしてるのはターミナル末尾列に書くと自動改行する端末があるため。
|
|
385
413
|
// truncateToCells は CJK / 絵文字を 2 セルとして正しく数える。
|
|
386
|
-
const columns =
|
|
387
|
-
? process.stdout.columns - 1
|
|
388
|
-
: 120;
|
|
414
|
+
const columns = resolveColumns();
|
|
389
415
|
const clipped = lines.map((l) => truncateToCells(l, columns));
|
|
390
416
|
|
|
391
417
|
// 前フレームを消去してから再描画:
|
|
@@ -520,6 +546,7 @@ export const _internal = {
|
|
|
520
546
|
resetRenderKeyCache,
|
|
521
547
|
formatTimeAgo,
|
|
522
548
|
shouldForceFullRedraw,
|
|
549
|
+
resolveColumns,
|
|
523
550
|
};
|
|
524
551
|
|
|
525
552
|
// --- エントリポイント自動起動 ---
|
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
formatLine,
|
|
16
16
|
formatTimeAgo,
|
|
17
17
|
shouldForceFullRedraw,
|
|
18
|
+
resolveColumns,
|
|
18
19
|
} = _internal;
|
|
19
20
|
|
|
20
21
|
// state-file は projectPath を resolve + lowercase 正規化する。
|
|
@@ -385,6 +386,58 @@ test('shouldForceFullRedraw: 片方 0 以下は false (未初期化 or 無効)',
|
|
|
385
386
|
assert.equal(shouldForceFullRedraw(0, 80), true);
|
|
386
387
|
});
|
|
387
388
|
|
|
389
|
+
// ─── resolveColumns ───────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
test('resolveColumns: process.stdout.columns が 40 以上ならそれ - 1 を返す', () => {
|
|
392
|
+
const orig = process.stdout.columns;
|
|
393
|
+
try {
|
|
394
|
+
Object.defineProperty(process.stdout, 'columns', { value: 120, configurable: true, writable: true });
|
|
395
|
+
assert.equal(resolveColumns(), 119);
|
|
396
|
+
} finally {
|
|
397
|
+
Object.defineProperty(process.stdout, 'columns', { value: orig, configurable: true, writable: true });
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
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;
|
|
407
|
+
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
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
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';
|
|
420
|
+
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
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
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;
|
|
434
|
+
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
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
388
441
|
// ─── formatLine: time-ago 表示 ────────────────────────────────────
|
|
389
442
|
|
|
390
443
|
test('formatLine: updatedAt から経過時間が表示される', () => {
|
|
@@ -395,12 +448,30 @@ test('formatLine: updatedAt から経過時間が表示される', () => {
|
|
|
395
448
|
};
|
|
396
449
|
args.state.updatedAt = now - 3 * 60 * 1000; // 3 分前
|
|
397
450
|
const out = stripColors(formatLine(args));
|
|
398
|
-
assert.ok(out.includes('
|
|
451
|
+
assert.ok(out.includes('3m ago'), `expected "3m ago" in output: ${out}`);
|
|
399
452
|
});
|
|
400
453
|
|
|
401
|
-
test('formatLine: updatedAt が無ければ ago
|
|
454
|
+
test('formatLine: updatedAt が無ければ ago テキストは空', () => {
|
|
402
455
|
const args = makeLineArgs(0.5);
|
|
403
456
|
delete args.state.updatedAt;
|
|
404
457
|
const out = stripColors(formatLine(args));
|
|
405
458
|
assert.ok(!out.includes('ago'));
|
|
459
|
+
assert.ok(!out.includes('just now'));
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('formatLine: ago は session id のすぐ右に配置される(狭幅でも切れない位置)', () => {
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
const args = {
|
|
465
|
+
...makeLineArgs(0.5),
|
|
466
|
+
now,
|
|
467
|
+
};
|
|
468
|
+
args.state.updatedAt = now - 5 * 60 * 1000; // 5m
|
|
469
|
+
const out = stripColors(formatLine(args));
|
|
470
|
+
// shortId と 5m ago の位置関係: shortId が先、ago がその後、bar (█ or ░) がさらに後
|
|
471
|
+
const idPos = out.indexOf('abc12345');
|
|
472
|
+
const agoPos = out.indexOf('5m ago');
|
|
473
|
+
const barPos = out.search(/[█░]/);
|
|
474
|
+
assert.ok(idPos >= 0 && agoPos >= 0 && barPos >= 0, 'all fields present');
|
|
475
|
+
assert.ok(idPos < agoPos, 'shortId before ago');
|
|
476
|
+
assert.ok(agoPos < barPos, 'ago before bar');
|
|
406
477
|
});
|
package/src/vscode-task.mjs
CHANGED
|
@@ -109,6 +109,12 @@ export function hasMonitorTask(obj) {
|
|
|
109
109
|
* 依存して失敗しうる。process は command を直接起動するため堅い。
|
|
110
110
|
* command には Node 実行ファイルの絶対パスを入れ、args に throughline.mjs の
|
|
111
111
|
* 絶対パスと 'monitor' サブコマンドを渡す。
|
|
112
|
+
*
|
|
113
|
+
* options.env.COLUMNS=200 を渡す理由:
|
|
114
|
+
* type: 'process' では VSCode は PTY を張らず stdout をパイプするため、
|
|
115
|
+
* 子プロセスの `process.stdout.columns` が undefined か極端に小さい値になり、
|
|
116
|
+
* モニターの幅計算が破綻する。COLUMNS env を明示しておけば token-monitor の
|
|
117
|
+
* resolveColumns() が 200 を採用して行が「openclaw」で切れる不具合を避けられる。
|
|
112
118
|
*/
|
|
113
119
|
export function buildMonitorTask(throughlineBin) {
|
|
114
120
|
return {
|
|
@@ -117,6 +123,9 @@ export function buildMonitorTask(throughlineBin) {
|
|
|
117
123
|
type: 'process',
|
|
118
124
|
command: process.execPath,
|
|
119
125
|
args: [throughlineBin, 'monitor'],
|
|
126
|
+
options: {
|
|
127
|
+
env: { COLUMNS: '200' },
|
|
128
|
+
},
|
|
120
129
|
isBackground: true,
|
|
121
130
|
presentation: {
|
|
122
131
|
reveal: 'always',
|
package/src/vscode-task.test.mjs
CHANGED
|
@@ -148,6 +148,13 @@ test('buildMonitorTask: uses type=process with provided bin as args[0]', () => {
|
|
|
148
148
|
assert.equal(task.isBackground, true);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
test('buildMonitorTask: sets COLUMNS=200 env to work around type:process non-TTY stdout', () => {
|
|
152
|
+
const task = buildMonitorTask('/abs/bin/throughline.mjs');
|
|
153
|
+
assert.ok(task.options, 'task should carry options');
|
|
154
|
+
assert.ok(task.options.env, 'options should carry env');
|
|
155
|
+
assert.equal(task.options.env.COLUMNS, '200');
|
|
156
|
+
});
|
|
157
|
+
|
|
151
158
|
// --- ensureMonitorTaskFile: skip conditions ---
|
|
152
159
|
|
|
153
160
|
test('ensureMonitorTaskFile: opt_out via THROUGHLINE_NO_VSCODE=1', () => {
|