throughline 0.16.16 → 0.16.17

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/CHANGELOG.md CHANGED
@@ -10,6 +10,30 @@ shipped to npm but were not individually tagged on GitHub.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.16.17] — 2026-10-10
14
+
15
+ ### 修正
16
+
17
+ - Windowsで、Claude Codeの自動継続の後、Windows Terminalの窓が約2秒おきに開いては消えた。後継をClaude Desktopへ移す処理は、後継が手すきになるまで`claude agents --json --all`を繰り返し呼ぶ。この処理はconsoleを持たない切り離したprocessで動き、子の`pwsh.exe`を窓を隠さずに起こしていたので、呼ぶたびに新しいconsole(既定のターミナルの窓)が出ていた。後継が手すきにならない間、最長2分、ターンが終わるたびに続いた。
18
+ - 子processを起こす共通の部品(`src/os/portable-spawn-sync.mjs`)が、窓を隠す指定(`windowsHide`)を既定で付ける。`claude agents`・`claude stop`・後継の起動・L1の要約(`claude -p`・`codex exec`)・更新の処理が対象。
19
+ - URLを開く`cmd.exe /c start`と、手動の引き継ぎのapp-serverの起動にも付けた。開く先のアプリの窓は隠れない。
20
+ - 端末が要る`claude --desktop --resume`は、今までどおり最小化した新しいconsoleの中で呼ぶ(1回だけ)。
21
+ - 手すきを待つ間隔を1秒から3秒へ広げた。
22
+
23
+ ### 対応の範囲
24
+
25
+ | | macOS | Linux | Windows |
26
+ |---|---|---|---|
27
+ | Claude Code | 窓の指定は効かないOS。手すきを待つ間隔だけ変わる(3秒)。単体試験とCI | 窓の指定は効かないOS。単体試験とCI。Desktopへ移す処理はLinuxでは動かない | 直した。実機の画面のあるsessionで、consoleを持たない切り離したprocessから`claude agents`を呼び、現れた窓を30msおきに数えた。0.16.16は3回の呼び出しでWindows Terminalの窓が3回、直した版は5回の呼び出しで0回。呼び出しの結果(一覧)は同じに読めた |
28
+ | Codex | 同じ(効かないOS) | 同じ | 直した。URLを開く所は、実機で窓を隠した`cmd.exe`からアプリが開く事を確かめた(電卓のURLで代用)。自動継続のworkerが起こすL1の要約(`codex exec`)は同じ部品を通るが、個別には確かめていない |
29
+ | Grok | 同じ(効かないOS) | 同じ | 直した。Stopのhookが起こすL1の要約が同じ部品を通る。個別には確かめていない |
30
+ | Cursor | 同じ(効かないOS) | 同じ | 直した。Grokと同じ |
31
+
32
+ ### 確認していない範囲
33
+
34
+ - 本物の引き継ぎの後に窓が出ない事(公開して端末へ入れた後に確かめる)。
35
+ - 後継が手すきにならない理由(裏で動き続ける処理を持つ会話など)。その間、後継はClaude Desktopへ移らない。
36
+
13
37
  ## [0.16.16] — 2026-10-10
14
38
 
15
39
  ### 追加
@@ -2246,7 +2270,8 @@ two attempts, instrument first instead of patching again.
2246
2270
 
2247
2271
  ---
2248
2272
 
2249
- [Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.16...HEAD
2273
+ [Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.17...HEAD
2274
+ [0.16.17]: https://github.com/kitepon/Throughline/compare/v0.16.16...v0.16.17
2250
2275
  [0.16.16]: https://github.com/kitepon/Throughline/compare/v0.16.15...v0.16.16
2251
2276
  [0.16.15]: https://github.com/kitepon/Throughline/compare/v0.16.14...v0.16.15
2252
2277
  [0.16.14]: https://github.com/kitepon/Throughline/compare/v0.16.13...v0.16.14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.16.16",
3
+ "version": "0.16.17",
4
4
  "type": "module",
5
5
  "description": "Persistent memory hooks for Claude Code, Codex, Grok, and Cursor (/clear-safe context compression)",
6
6
  "keywords": [
@@ -778,7 +778,8 @@ export async function runClaudeDesktopOpen(sourceSessionId, {
778
778
  spawn = spawnPortableSync,
779
779
  pty = runWithPty,
780
780
  idleTimeoutMs = 120_000,
781
- pollMs = 1_000,
781
+ // 一覧を読むたびに `claude agents` を起こす(Windows では pwsh 経由)。手すきは急いで知る必要が無いので、間を空ける。
782
+ pollMs = 3_000,
782
783
  } = {}) {
783
784
  let record = readClaudeAutoHandoff(sourceSessionId, dir);
784
785
  if (!record) throw new ClaudeHandoffError('handoff_not_found');
@@ -1014,7 +1014,7 @@ export async function run(argv = []) {
1014
1014
  await check('throughline コマンドが PATH で見つかるか', () => {
1015
1015
  try {
1016
1016
  const which = process.platform === 'win32' ? 'where throughline' : 'which throughline';
1017
- const result = execSync(which, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
1017
+ const result = execSync(which, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }).trim();
1018
1018
  return result.split(/\r?\n/)[0];
1019
1019
  } catch {
1020
1020
  throw new Error('見つからない — npm install -g throughline を実行してください');
@@ -1649,6 +1649,7 @@ function startAppServerClient({ command, args, cwd, timeoutMs, requestTimeoutMs
1649
1649
  const child = spawn(command, args, {
1650
1650
  cwd,
1651
1651
  stdio: ['pipe', 'pipe', 'pipe'],
1652
+ windowsHide: true,
1652
1653
  });
1653
1654
  let stdoutBuffer = '';
1654
1655
  let stderr = '';
@@ -11,6 +11,7 @@ export function openUrlWithOsHandler(url, {
11
11
  spawnImpl = spawnSync,
12
12
  } = {}) {
13
13
  if (platform === 'darwin') return spawnImpl('open', [url], { encoding: 'utf8' });
14
- if (platform === 'win32') return spawnImpl('cmd.exe', ['/c', 'start', '', url], { encoding: 'utf8' });
14
+ // cmd.exe 自身の窓は隠す(console を持たない worker から呼ぶと、窓が一瞬出る)。start が開く先の窓は、これでは隠れない。
15
+ if (platform === 'win32') return spawnImpl('cmd.exe', ['/c', 'start', '', url], { encoding: 'utf8', windowsHide: true });
15
16
  return spawnImpl('xdg-open', [url], { encoding: 'utf8' });
16
17
  }
@@ -0,0 +1,14 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { openUrlWithOsHandler } from './open-url.mjs';
4
+
5
+ test('openUrlWithOsHandler: OS ごとの命令で開き、Windows では cmd.exe の窓を隠す', () => {
6
+ const calls = [];
7
+ const spawnImpl = (command, args, options) => { calls.push({ command, args, options }); return { status: 0 }; };
8
+ openUrlWithOsHandler('codex://threads/abc', { platform: 'win32', spawnImpl });
9
+ openUrlWithOsHandler('codex://threads/abc', { platform: 'darwin', spawnImpl });
10
+ openUrlWithOsHandler('codex://threads/abc', { platform: 'linux', spawnImpl });
11
+ assert.deepEqual(calls[0], { command: 'cmd.exe', args: ['/c', 'start', '', 'codex://threads/abc'], options: { encoding: 'utf8', windowsHide: true } });
12
+ assert.deepEqual([calls[1].command, calls[1].args], ['open', ['codex://threads/abc']]);
13
+ assert.deepEqual([calls[2].command, calls[2].args], ['xdg-open', ['codex://threads/abc']]);
14
+ });
@@ -28,11 +28,15 @@ function resolveWindowsCommand(command, env) {
28
28
  return command;
29
29
  }
30
30
 
31
- function portableInvocation(command, args, options = {}) {
31
+ export function portableInvocation(command, args, options = {}) {
32
32
  const platform = options.platform ?? process.platform;
33
33
  const spawnOptions = { ...options };
34
34
  delete spawnOptions.platform;
35
35
  spawnOptions.shell = false;
36
+ // Windows では、console を持たない process(切り離した worker など)が console の子を起こすと、新しい console が作られ、
37
+ // 既定のターミナル(Windows Terminal)の窓が一瞬出る。出力は pipe で受けるので、窓は要らない。
38
+ // 窓が要る呼び出し(端末が要る命令)は os/pty-run.mjs が別に持つ。ほかの OS では無視される。
39
+ spawnOptions.windowsHide ??= true;
36
40
 
37
41
  if (platform !== 'win32') return [command, args, spawnOptions];
38
42
 
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
3
3
  import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
- import { spawnPortable, spawnPortableSync } from './portable-spawn-sync.mjs';
6
+ import { portableInvocation, spawnPortable, spawnPortableSync } from './portable-spawn-sync.mjs';
7
7
  import { createInterface } from 'node:readline';
8
8
  import { once } from 'node:events';
9
9
 
@@ -88,3 +88,24 @@ exit $LASTEXITCODE
88
88
  rmSync(dir, { recursive: true, force: true });
89
89
  }
90
90
  });
91
+
92
+ test('portableInvocation: 窓を隠す指定を既定で付ける。呼び出し側が false を渡した時だけ外す', () => {
93
+ for (const platform of ['win32', 'linux', 'darwin']) {
94
+ const [, , hidden] = portableInvocation('claude', ['agents', '--json', '--all'], { platform, env: { PATH: '' }, encoding: 'utf8' });
95
+ assert.equal(hidden.windowsHide, true, platform);
96
+ assert.equal(hidden.shell, false); assert.equal(hidden.encoding, 'utf8'); assert.equal('platform' in hidden, false);
97
+ const [, , shown] = portableInvocation('claude', ['agents'], { platform, env: { PATH: '' }, windowsHide: false });
98
+ assert.equal(shown.windowsHide, false, platform);
99
+ }
100
+ });
101
+
102
+ test('portableInvocation: Windows の .ps1 shim は pwsh.exe 経由で、窓を隠して起こす', () => {
103
+ const dir = mkdtempSync(join(tmpdir(), 'tl-portable-hide-'));
104
+ try {
105
+ writeFileSync(join(dir, 'claude.ps1'), 'exit 0\n');
106
+ const [command, args, options] = portableInvocation('claude', ['stop', 'abcd1234'], { platform: 'win32', env: { PATH: dir } });
107
+ assert.equal(command, 'pwsh.exe');
108
+ assert.ok(args.at(-1).includes(join(dir, 'claude.ps1')) && args.at(-1).includes("'stop' 'abcd1234'"));
109
+ assert.equal(options.windowsHide, true);
110
+ } finally { rmSync(dir, { recursive: true, force: true }); }
111
+ });