throughline 0.3.5 → 0.3.7
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 +32 -3
- package/src/token-monitor.test.mjs +59 -0
- 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,36 @@ export function shouldForceFullRedraw(prevCols, currCols) {
|
|
|
231
231
|
return prevCols !== currCols && currCols > 0;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* 描画に使う列幅を解決する。
|
|
236
|
+
*
|
|
237
|
+
* 優先順:
|
|
238
|
+
* 1. **stdout が TTY** かつ `process.stdout.columns` が 40 以上
|
|
239
|
+
* → その値から 1 引いたもの(末尾列での自動改行回避)
|
|
240
|
+
* 2. `process.env.COLUMNS` が 40 以上 → その値 - 1
|
|
241
|
+
* 3. それ以外 → 200 にフォールバック
|
|
242
|
+
*
|
|
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 にフォールバックする。
|
|
250
|
+
*
|
|
251
|
+
* 200 固定フォールバックは、truncateToCells が 200 セル以下の実内容をそのまま通す
|
|
252
|
+
* (伸長しない)ので過大でも副作用なし。
|
|
253
|
+
*/
|
|
254
|
+
export function resolveColumns() {
|
|
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
|
+
}
|
|
259
|
+
const fromEnv = Number(process.env.COLUMNS);
|
|
260
|
+
if (Number.isFinite(fromEnv) && fromEnv >= 40) return fromEnv - 1;
|
|
261
|
+
return 200;
|
|
262
|
+
}
|
|
263
|
+
|
|
234
264
|
function formatLine({ state, usage, isActive, now = Date.now() }) {
|
|
235
265
|
const project = basename(state.projectPath || '?');
|
|
236
266
|
const shortId = state.sessionId.slice(0, 8);
|
|
@@ -387,9 +417,7 @@ function renderFrame(args) {
|
|
|
387
417
|
// こうすれば ANSI.up(lines.length) と論理行数が物理行数と一致する。
|
|
388
418
|
// columns - 1 にしてるのはターミナル末尾列に書くと自動改行する端末があるため。
|
|
389
419
|
// truncateToCells は CJK / 絵文字を 2 セルとして正しく数える。
|
|
390
|
-
const columns =
|
|
391
|
-
? process.stdout.columns - 1
|
|
392
|
-
: 120;
|
|
420
|
+
const columns = resolveColumns();
|
|
393
421
|
const clipped = lines.map((l) => truncateToCells(l, columns));
|
|
394
422
|
|
|
395
423
|
// 前フレームを消去してから再描画:
|
|
@@ -524,6 +552,7 @@ export const _internal = {
|
|
|
524
552
|
resetRenderKeyCache,
|
|
525
553
|
formatTimeAgo,
|
|
526
554
|
shouldForceFullRedraw,
|
|
555
|
+
resolveColumns,
|
|
527
556
|
};
|
|
528
557
|
|
|
529
558
|
// --- エントリポイント自動起動 ---
|
|
@@ -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,64 @@ test('shouldForceFullRedraw: 片方 0 以下は false (未初期化 or 無効)',
|
|
|
385
386
|
assert.equal(shouldForceFullRedraw(0, 80), true);
|
|
386
387
|
});
|
|
387
388
|
|
|
389
|
+
// ─── resolveColumns ───────────────────────────────────────────────
|
|
390
|
+
|
|
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;
|
|
395
|
+
try {
|
|
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();
|
|
401
|
+
} finally {
|
|
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;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
test('resolveColumns: TTY かつ columns が 40 以上 → その値 - 1', () => {
|
|
410
|
+
withStdoutState({ isTTY: true, columns: 120, envColumns: undefined }, () => {
|
|
411
|
+
assert.equal(resolveColumns(), 119);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('resolveColumns: 非 TTY なら columns が大きくても信用しない (env も 200 も使う)', () => {
|
|
416
|
+
// type: process タスクで columns が 120 にセットされたが実際の幅とは連動しない、という状況。
|
|
417
|
+
// env.COLUMNS も無ければ 200 フォールバック。
|
|
418
|
+
withStdoutState({ isTTY: false, columns: 120, envColumns: undefined }, () => {
|
|
419
|
+
assert.equal(resolveColumns(), 200);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
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' }, () => {
|
|
431
|
+
assert.equal(resolveColumns(), 149);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test('resolveColumns: TTY で columns 未設定、env も無ければフォールバック 200', () => {
|
|
436
|
+
withStdoutState({ isTTY: true, columns: undefined, envColumns: undefined }, () => {
|
|
437
|
+
assert.equal(resolveColumns(), 200);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('resolveColumns: 全てのソースが無効ならフォールバック 200', () => {
|
|
442
|
+
withStdoutState({ isTTY: false, columns: undefined, envColumns: undefined }, () => {
|
|
443
|
+
assert.equal(resolveColumns(), 200);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
388
447
|
// ─── formatLine: time-ago 表示 ────────────────────────────────────
|
|
389
448
|
|
|
390
449
|
test('formatLine: updatedAt から経過時間が表示される', () => {
|
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', () => {
|