throughline 0.3.17 → 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/README.md +11 -3
- package/package.json +1 -1
- package/src/prompt-submit.mjs +11 -0
- package/src/session-start.mjs +13 -0
- package/src/vscode-task.mjs +33 -0
- package/src/vscode-task.test.mjs +58 -0
package/README.md
CHANGED
|
@@ -178,9 +178,16 @@ Example output (real values from a running 1M-context Opus session):
|
|
|
178
178
|
- **Line-wrap safe.** Each line is truncated to `process.stdout.columns - 1`
|
|
179
179
|
before drawing, preserving ANSI color codes. The redraw cursor math cannot
|
|
180
180
|
desync on narrow terminals.
|
|
181
|
-
- **Resize resilient
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
- **Resize resilient via OSC 18t.** Windows ConPTY + VS Code task terminals
|
|
182
|
+
freeze `process.stdout.columns` at the PTY's initial size and never
|
|
183
|
+
propagate panel resizes into Node, so polling or `resize` events can't
|
|
184
|
+
catch them. Throughline queries the terminal itself with the CSI `18 t`
|
|
185
|
+
escape (`\x1b[18t`) every tick, parses the `\x1b[8;rows;cols t` reply off
|
|
186
|
+
stdin in raw mode, and uses the real current width for truncation. On
|
|
187
|
+
terminals that don't answer the query, the renderer falls back to
|
|
188
|
+
`process.stdout.columns → env.COLUMNS → 80`. When the width changes the
|
|
189
|
+
viewport is cleared in full (`\x1b[2J\x1b[3J\x1b[H`) before the next
|
|
190
|
+
frame so the previous, wrongly-sized frame can't stack beneath it.
|
|
184
191
|
- **Per-row "last updated" stamp.** Each session row carries an 8-cell
|
|
185
192
|
`just now` / `24m ago` stamp right after the session id, placed before the
|
|
186
193
|
bar so narrow terminals don't truncate it. It resets to `just now` on every
|
|
@@ -256,6 +263,7 @@ entry to the `tasks` array yourself:
|
|
|
256
263
|
| `throughline install --project` | Register hooks in `.claude/settings.json` for this repo only |
|
|
257
264
|
| `throughline uninstall` | Remove Throughline hooks from the settings file |
|
|
258
265
|
| `throughline monitor [--all] [--session <id>]` | Run the multi-session token monitor |
|
|
266
|
+
| `throughline monitor --diag` | Dump TTY/columns/env diagnostics (for debugging monitor render bugs) |
|
|
259
267
|
| `throughline detail <time>` | Retrieve L2 body text and L3 tool I/O for a turn (see below) |
|
|
260
268
|
| `throughline save-inflight` | Called by `/tl` to attach an in-flight memo (stdin) to the current baton |
|
|
261
269
|
| `throughline doctor` | Check Node version, hook registration, DB writability, PATH |
|
package/package.json
CHANGED
package/src/prompt-submit.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { getDb } from './db.mjs';
|
|
16
16
|
import { writeBaton } from './baton.mjs';
|
|
17
|
+
import { ensureMonitorTaskFile } from './vscode-task.mjs';
|
|
17
18
|
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
18
19
|
import { join, dirname } from 'node:path';
|
|
19
20
|
import { homedir } from 'node:os';
|
|
@@ -54,6 +55,16 @@ async function main() {
|
|
|
54
55
|
const payload = JSON.parse(raw);
|
|
55
56
|
const { session_id, cwd, prompt } = payload;
|
|
56
57
|
|
|
58
|
+
// VSCode 新規プロジェクトへの tasks.json 自動プロビジョニング。
|
|
59
|
+
// SessionStart/Stop に加えここでも呼ぶことで、どれか 1 つでも発火すれば初回メッセージ送信で
|
|
60
|
+
// tasks.json が生える。冪等性は ensureMonitorTaskFile 側で保証。/tl 判定より前に置く。
|
|
61
|
+
try {
|
|
62
|
+
ensureMonitorTaskFile({ cwd: cwd ?? process.cwd(), env: process.env });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
65
|
+
process.stderr.write(`[vscode-task] ${msg}\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
if (!isBatonCommand(prompt)) {
|
|
58
69
|
process.exit(0);
|
|
59
70
|
return;
|
package/src/session-start.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { getDb } from './db.mjs';
|
|
|
21
21
|
import { consumeBaton } from './baton.mjs';
|
|
22
22
|
import { mergeSpecificPredecessor, resolveMergeTarget } from './session-merger.mjs';
|
|
23
23
|
import { buildResumeContext } from './resume-context.mjs';
|
|
24
|
+
import { ensureMonitorTaskFile } from './vscode-task.mjs';
|
|
24
25
|
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
25
26
|
import { join, dirname } from 'node:path';
|
|
26
27
|
import { homedir } from 'node:os';
|
|
@@ -55,6 +56,18 @@ async function main() {
|
|
|
55
56
|
const db = getDb();
|
|
56
57
|
const now = Date.now();
|
|
57
58
|
|
|
59
|
+
// 0. VSCode で開かれた新規プロジェクトに .vscode/tasks.json を自動プロビジョニング。
|
|
60
|
+
// Stop hook 側にも同じ呼び出しがあるが、Stop が発火しない環境(応答中断・IDE 挙動差)
|
|
61
|
+
// でも SessionStart は必ず走るので、新規プロジェクトでの自動起動を確実化する保険。
|
|
62
|
+
// 冪等性は ensureMonitorTaskFile 側で保証されており、Stop/UserPromptSubmit と重複呼び
|
|
63
|
+
// 出しされても安全。
|
|
64
|
+
try {
|
|
65
|
+
ensureMonitorTaskFile({ cwd: projectPath, env: process.env });
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
68
|
+
process.stderr.write(`[vscode-task] ${msg}\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
58
71
|
// 1. sessions テーブルに INSERT OR IGNORE
|
|
59
72
|
db.prepare(
|
|
60
73
|
`INSERT OR IGNORE INTO sessions (session_id, project_path, status, created_at, updated_at)
|
package/src/vscode-task.mjs
CHANGED
|
@@ -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
|
}
|
package/src/vscode-task.test.mjs
CHANGED
|
@@ -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
|
+
});
|