@cocorograph/hub-agent 0.8.20 → 0.8.22
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/bin/hub-agent.mjs +0 -46
- package/package.json +1 -1
- package/src/claude-md-render.mjs +222 -0
- package/src/claude-md-sync.mjs +545 -136
- package/src/claude-md.mjs +272 -250
- package/src/codex-stream-bridge.mjs +6 -2
- package/src/hub-request-deadline.mjs +273 -0
- package/src/main.mjs +0 -108
- package/src/state.mjs +5 -15
- package/src/tmux.mjs +55 -188
- package/src/usage.mjs +1 -1
- package/src/codex-rollout-watch.mjs +0 -148
- package/src/session-mirror.mjs +0 -190
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub API 呼び出しの期限(1 往復ごと)と、セッション起動あたりの全体予算。
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ ここが保証するのは次の 3 つ **だけ**。これを超える断定(「同期全体が N 秒以内」
|
|
5
|
+
* 「セッション起動が必ず N 秒で終わる」等)を書かないこと:
|
|
6
|
+
* 1. 各 HTTP 往復(headers 受信 〜 body 消費)は `HUB_REQUEST_TIMEOUT_MS` で打ち切る
|
|
7
|
+
* 2. `ensureClaudeMd` + `syncClaudeMdWithHub` は、**予算の生成時から数えた経過時間**が
|
|
8
|
+
* `SESSION_BUDGET_MS` を超えたら打ち切る(セッション起動あたり 1 本の予算)
|
|
9
|
+
* 3. 予算は **wall-clock の期限**であり、通信の待ち時間を積み上げた総量ではない。
|
|
10
|
+
* ファイル I/O・レンダリング・イベントループの停止も残量を減らす。ただし
|
|
11
|
+
* **打ち切る対象は Hub 往復と、その後に続く同期の処理**であって、それ以外の
|
|
12
|
+
* 処理を中断はしない
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ 3 を「通信の待ち時間だけを積算する」実装に変えないこと。director と
|
|
15
|
+
* repositories は 1 本の deadline を共有して**並列**に待つので待ち時間が二重計上され、
|
|
16
|
+
* かつ CPU が詰まっている間・スリープ中は残量が減らないので **上限が事実上無制限**に
|
|
17
|
+
* なる。wall-clock 上限のままにしておくことが「セッション起動を有限にする」の根拠。
|
|
18
|
+
*
|
|
19
|
+
* `src/claude-md.mjs`(初期化)と `src/claude-md-sync.mjs`(双方向同期)の両方から
|
|
20
|
+
* 使うため独立モジュールにしてある。**2 系統に複製しないこと**(片方だけ直して
|
|
21
|
+
* ドリフトする)。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hub API **1 往復あたり**(headers 受信 〜 body 消費)に掛ける既定の期限 (ms)。
|
|
26
|
+
*
|
|
27
|
+
* 15 秒とした根拠: 短くしたい誘惑があるが、低速回線・スリープ復帰直後の**正常な**
|
|
28
|
+
* 往復を恒常的に失敗させる方が害が大きい(同期が毎回諦めると正本がドリフトし続ける)。
|
|
29
|
+
* 健全な回線での往復は 1 秒程度で完了するので、15 秒は「遅いが正常」を通しつつ
|
|
30
|
+
* 「返ってこない」を切るための余裕として置いている。
|
|
31
|
+
* ⚠️ **本文サイズを根拠に書かないこと。** CLAUDE.md 正本の大きさは案件ごとに違い、
|
|
32
|
+
* 運用のなかで増減する。具体値を書くと必ず陳腐化する(旧コメントに残っていた
|
|
33
|
+
* 「往復は本文が小さいから速い」という断定は、実データの正本サイズと矛盾していた)。
|
|
34
|
+
* ⚠️ 導入前は **無制限**(body を閉じないレスポンスで往復が永久に返らない)だった。
|
|
35
|
+
*/
|
|
36
|
+
export const HUB_REQUEST_TIMEOUT_MS = 15_000
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* セッション起動 1 回あたりの **wall-clock 予算** (ms)。予算の生成時から数えた
|
|
40
|
+
* 残り時間で、通信の待ち時間を積み上げた総量ではない。
|
|
41
|
+
*
|
|
42
|
+
* 1 往復に期限があっても、初期化と同期は **複数往復**を行うため、その本数だけ
|
|
43
|
+
* 足し算されて `createSession` が止まる時間の上限が決まらない。そこで「起動 1 回で
|
|
44
|
+
* 使ってよい wall-clock」をここで決め、超えたら **何も書かずに次回へ持ち越す**
|
|
45
|
+
* (不確定な状態で fallback 本文を書かない)。
|
|
46
|
+
* ⚠️ 打ち切る対象は **Hub 往復と、それに続く同期の処理**だけ。ファイル I/O・CPU 待ち・
|
|
47
|
+
* スリープも残量を減らすが、それらを直接中断はしない(「セッション起動全体が N 秒
|
|
48
|
+
* 以内」を保証するものではない。このモジュール冒頭の 3 が正本)。
|
|
49
|
+
* ⚠️ 往復の本数を具体的に列挙しないこと。経路が増減するたびにコメントだけが
|
|
50
|
+
* 古くなり、実在しない往復を数え続ける(過去に実際そうなった)。
|
|
51
|
+
*/
|
|
52
|
+
export const SESSION_BUDGET_MS = 30_000
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 1 往復ぶんの期限とキャンセル手段をまとめたもの。
|
|
56
|
+
*
|
|
57
|
+
* ⚠️ `Promise.race` だけでは**負けた側(`fetch` / `res.json()` / body 読み)が裏で
|
|
58
|
+
* 走り続ける**ため、期限超過時は必ず `AbortController.abort()`(+ 呼び出し側での
|
|
59
|
+
* `reader.cancel()`)を行い、正常終了時は `clear()` でタイマーを解除すること。
|
|
60
|
+
* タイマーは `unref()` して、これだけでプロセスが生き残らないようにする。
|
|
61
|
+
*
|
|
62
|
+
* @param {number} ms 期限。**0 以下なら生成時点で期限切れ**(= `guard` は即
|
|
63
|
+
* `{timedOut:true}`)。予算を使い切った後に「まだ少し待てる」と誤認しないため。
|
|
64
|
+
* @param {object} [opts]
|
|
65
|
+
* @param {AbortSignal} [opts.parentSignal] 親(=セッション全体予算)の signal。
|
|
66
|
+
* 親が abort したら子も abort し、`guard` を待っている呼び出しも解ける。
|
|
67
|
+
*/
|
|
68
|
+
export function createDeadline(ms, { parentSignal = null } = {}) {
|
|
69
|
+
const controller = new AbortController()
|
|
70
|
+
let fire = null
|
|
71
|
+
const expiredPromise = new Promise((resolve) => {
|
|
72
|
+
fire = resolve
|
|
73
|
+
})
|
|
74
|
+
let fired = false
|
|
75
|
+
const expire = () => {
|
|
76
|
+
if (fired) return
|
|
77
|
+
fired = true
|
|
78
|
+
try {
|
|
79
|
+
controller.abort()
|
|
80
|
+
} catch {
|
|
81
|
+
/* noop */
|
|
82
|
+
}
|
|
83
|
+
fire()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ⚠️ `AbortSignal.any` は Node 20.3+。engines は ">=20" なので **存在確認してから**
|
|
87
|
+
// 使い、無い環境では親 signal のリスナーで子 controller を abort する。
|
|
88
|
+
const hasAbortSignalAny = typeof AbortSignal !== "undefined" && typeof AbortSignal.any === "function"
|
|
89
|
+
let signal = controller.signal
|
|
90
|
+
let detachParent = () => {}
|
|
91
|
+
if (parentSignal) {
|
|
92
|
+
if (hasAbortSignalAny) {
|
|
93
|
+
// 合成 signal を fetch に渡す(親・子どちらの abort でも中断される)。
|
|
94
|
+
try {
|
|
95
|
+
signal = AbortSignal.any([controller.signal, parentSignal])
|
|
96
|
+
} catch {
|
|
97
|
+
signal = controller.signal
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (parentSignal.aborted) {
|
|
101
|
+
// 親が既に切れている=残り 0。生成時点で期限切れにする。
|
|
102
|
+
if (!hasAbortSignalAny) expire()
|
|
103
|
+
else {
|
|
104
|
+
fired = true
|
|
105
|
+
fire()
|
|
106
|
+
}
|
|
107
|
+
} else if (typeof parentSignal.addEventListener === "function") {
|
|
108
|
+
const onParentAbort = () => {
|
|
109
|
+
// any が使える環境では合成 signal 側が既に abort 済みなので、ここでは
|
|
110
|
+
// 「待っている guard を解く」ことだけを行えばよい。使えない環境では
|
|
111
|
+
// 子 controller を abort して signal を伝播させる。
|
|
112
|
+
if (hasAbortSignalAny) {
|
|
113
|
+
if (!fired) {
|
|
114
|
+
fired = true
|
|
115
|
+
fire()
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
expire()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
parentSignal.addEventListener("abort", onParentAbort, { once: true })
|
|
122
|
+
detachParent = () => {
|
|
123
|
+
try {
|
|
124
|
+
parentSignal.removeEventListener("abort", onParentAbort)
|
|
125
|
+
} catch {
|
|
126
|
+
/* noop */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let timer = null
|
|
133
|
+
if (ms > 0) {
|
|
134
|
+
timer = setTimeout(expire, ms)
|
|
135
|
+
if (typeof timer.unref === "function") timer.unref()
|
|
136
|
+
} else {
|
|
137
|
+
// 0 以下 = 生成時点で期限切れ。タイマーを 1 tick 待たせない。
|
|
138
|
+
expire()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
get signal() {
|
|
143
|
+
return signal
|
|
144
|
+
},
|
|
145
|
+
/** 期限切れ済みか(`clear()` 後も状態は変わらない)。 */
|
|
146
|
+
expired() {
|
|
147
|
+
return fired
|
|
148
|
+
},
|
|
149
|
+
abort() {
|
|
150
|
+
try {
|
|
151
|
+
controller.abort()
|
|
152
|
+
} catch {
|
|
153
|
+
/* noop */
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
clear() {
|
|
157
|
+
if (timer) clearTimeout(timer)
|
|
158
|
+
detachParent()
|
|
159
|
+
},
|
|
160
|
+
/**
|
|
161
|
+
* `promise` を期限付きで待つ。
|
|
162
|
+
* @returns {Promise<{timedOut: boolean, value?: any, error?: any}>}
|
|
163
|
+
* 期限超過なら `{timedOut:true}`。負けた promise には必ずハンドラを付けて
|
|
164
|
+
* unhandled rejection にしない。
|
|
165
|
+
*/
|
|
166
|
+
async guard(promise) {
|
|
167
|
+
return await Promise.race([
|
|
168
|
+
promise.then(
|
|
169
|
+
(value) => ({ timedOut: false, value }),
|
|
170
|
+
(error) => ({ timedOut: false, error }),
|
|
171
|
+
),
|
|
172
|
+
expiredPromise.then(() => ({ timedOut: true })),
|
|
173
|
+
])
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* セッション起動 1 回ぶんの **全体予算**(生成時からの wall-clock 期限)。
|
|
180
|
+
*
|
|
181
|
+
* ⚠️ 「通信の待ち時間の累積」ではない。`createSessionBudget(20)` を作って HTTP を
|
|
182
|
+
* 一度も呼ばずに 20ms 超待つと `expired()` は `true` になる。ファイル I/O・呼び出し元の
|
|
183
|
+
* 処理時間も残量を減らす(このモジュール冒頭の 3 を参照)。
|
|
184
|
+
*
|
|
185
|
+
* 使い方(呼び出し元は 1 本だけ作り、`finally` で必ず `clear()` する):
|
|
186
|
+
* const budget = createSessionBudget()
|
|
187
|
+
* try { await ensureClaudeMd({ ..., budget }); await syncClaudeMdWithHub({ ..., budget }) }
|
|
188
|
+
* finally { budget.clear() }
|
|
189
|
+
*
|
|
190
|
+
* 予算が尽きたら **書き込みを行わずに諦める**(`ensureClaudeMd` は `deferred`、
|
|
191
|
+
* `syncClaudeMdWithHub` は `skipped`)。どちらもセッション起動は止めない。
|
|
192
|
+
*/
|
|
193
|
+
export function createSessionBudget(ms = SESSION_BUDGET_MS) {
|
|
194
|
+
const controller = new AbortController()
|
|
195
|
+
const budgetMs = Math.max(0, ms)
|
|
196
|
+
const deadlineAt = Date.now() + budgetMs
|
|
197
|
+
const children = new Set()
|
|
198
|
+
|
|
199
|
+
const abortAll = () => {
|
|
200
|
+
try {
|
|
201
|
+
controller.abort()
|
|
202
|
+
} catch {
|
|
203
|
+
/* noop */
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
let timer = null
|
|
207
|
+
if (budgetMs > 0) {
|
|
208
|
+
timer = setTimeout(abortAll, budgetMs)
|
|
209
|
+
if (typeof timer.unref === "function") timer.unref()
|
|
210
|
+
} else {
|
|
211
|
+
abortAll()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const remainingMs = () => Math.max(0, deadlineAt - Date.now())
|
|
215
|
+
const expired = () => controller.signal.aborted || remainingMs() <= 0
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
signal: controller.signal,
|
|
219
|
+
remainingMs,
|
|
220
|
+
expired,
|
|
221
|
+
/**
|
|
222
|
+
* 1 往復ぶんの子 deadline を作る。期限は「1 往復の上限」と「予算の残り」の小さい方。
|
|
223
|
+
* 残りが 0 なら **生成時点で期限切れ** の子を返す(待たせない)。
|
|
224
|
+
*/
|
|
225
|
+
childDeadline(perRequestMs = HUB_REQUEST_TIMEOUT_MS) {
|
|
226
|
+
const remaining = remainingMs()
|
|
227
|
+
const child = createDeadline(
|
|
228
|
+
expired() ? 0 : Math.min(perRequestMs, remaining),
|
|
229
|
+
{ parentSignal: controller.signal },
|
|
230
|
+
)
|
|
231
|
+
children.add(child)
|
|
232
|
+
return child
|
|
233
|
+
},
|
|
234
|
+
/**
|
|
235
|
+
* 予算の後片付け。
|
|
236
|
+
*
|
|
237
|
+
* ⚠️ **子 deadline を abort してから** タイマーを外すこと。`clear()` は
|
|
238
|
+
* タイマーとリスナーを外すだけで signal を abort しないため、先に clear だけ
|
|
239
|
+
* すると「非 OK で早期 return したが body を閉じない応答」のように、まだ
|
|
240
|
+
* 生きているソケットを解放する手段が誰にも残らない(実測で、`ensureClaudeMd`
|
|
241
|
+
* が返った後も 3 本のソケットが接続中のままになった)。
|
|
242
|
+
*
|
|
243
|
+
* 呼び出し元(`src/tmux.mjs`)は初期化と同期の await が両方終わった後の
|
|
244
|
+
* `finally` で呼ぶので、ここで abort しても正常な往復は殺さない。
|
|
245
|
+
*/
|
|
246
|
+
clear() {
|
|
247
|
+
if (timer) clearTimeout(timer)
|
|
248
|
+
for (const child of children) {
|
|
249
|
+
try {
|
|
250
|
+
child.abort()
|
|
251
|
+
} catch {
|
|
252
|
+
/* noop */
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
child.clear()
|
|
256
|
+
} catch {
|
|
257
|
+
/* noop */
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
children.clear()
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* budget があれば子 deadline を、無ければ単独 deadline を作る。
|
|
267
|
+
*
|
|
268
|
+
* 「budget 省略時も従来どおり 1 往復の期限だけは掛かる」を 1 箇所に集約する
|
|
269
|
+
* (呼び出し側に三項演算子を散らすと、片方だけ budget を渡し忘れる)。
|
|
270
|
+
*/
|
|
271
|
+
export function acquireDeadline(budget, perRequestMs = HUB_REQUEST_TIMEOUT_MS) {
|
|
272
|
+
return budget ? budget.childDeadline(perRequestMs) : createDeadline(perRequestMs)
|
|
273
|
+
}
|
package/src/main.mjs
CHANGED
|
@@ -103,8 +103,6 @@ import {
|
|
|
103
103
|
removeWorktree as removeWorktreeDir,
|
|
104
104
|
setSessionMouse,
|
|
105
105
|
setTmuxGlobalEnv,
|
|
106
|
-
startTerminalMirror,
|
|
107
|
-
stopTerminalMirror,
|
|
108
106
|
} from "./tmux.mjs"
|
|
109
107
|
import { SiblingInjectBridge } from "./sibling-inject.mjs"
|
|
110
108
|
import { isSessionWarm } from "./live-sessions.mjs"
|
|
@@ -1753,28 +1751,6 @@ async function readAllSessionEvents() {
|
|
|
1753
1751
|
// 共有し、同時要求は single-flight に畳む。create/kill/worktree 変更時は明示 clear する。
|
|
1754
1752
|
const tmuxSessionsSnapshotCache = new AsyncTtlCache({ ttlMs: 5_000 })
|
|
1755
1753
|
|
|
1756
|
-
/**
|
|
1757
|
-
* 単一 session の最新 session-event をファイル正本から読む。無ければ null。
|
|
1758
|
-
* claude.tui.bind 時の `stop` 再配信 (取りこぼし回復) に使う。
|
|
1759
|
-
*/
|
|
1760
|
-
async function readSessionEventFile(sessionName) {
|
|
1761
|
-
if (!sessionName || /[/\\]/.test(sessionName)) return null
|
|
1762
|
-
try {
|
|
1763
|
-
const text = await readFile(
|
|
1764
|
-
path.join(SESSION_EVENTS_DIR, `${sessionName}.json`),
|
|
1765
|
-
"utf-8",
|
|
1766
|
-
)
|
|
1767
|
-
const data = JSON.parse(text)
|
|
1768
|
-
if (!data || typeof data.event !== "string") return null
|
|
1769
|
-
return {
|
|
1770
|
-
event: data.event,
|
|
1771
|
-
at: typeof data.at === "number" ? data.at : Date.now(),
|
|
1772
|
-
}
|
|
1773
|
-
} catch {
|
|
1774
|
-
return null
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
1754
|
/**
|
|
1779
1755
|
* `/tmp/cockpit_session_events/<tmux_session>.json` を fs.watch して
|
|
1780
1756
|
* UserPromptSubmit / Stop の event を `session.event` 型で Hub に push する。
|
|
@@ -4039,90 +4015,6 @@ async function dispatch(msg, ctx) {
|
|
|
4039
4015
|
if (!ctx.claudeBridge) return
|
|
4040
4016
|
ctx.claudeBridge.interrupt({ stream_id: msg.stream_id })
|
|
4041
4017
|
return
|
|
4042
|
-
case "terminal.mirror.start": {
|
|
4043
|
-
// Cockpit Codex 両対応 Stream C (2026-07-03): チャット(SDK/app-server)の会話を
|
|
4044
|
-
// 変えずに、ターミナルのペイン (普通のシェルのまま) へ読み取り専用の実況を
|
|
4045
|
-
// 追加する。詳細は tmux.mjs の startTerminalMirror / session-mirror.mjs 参照。
|
|
4046
|
-
//
|
|
4047
|
-
// session_id はブラウザに要求しない。cwd から hub-agent 側でライブなチャット
|
|
4048
|
-
// session を解決する (claudeBridge.liveSessionIdForCwd / codexBridge.sessions を
|
|
4049
|
-
// cwd で検索)。これにより frontend は「今この cwd で実際に動いているチャット」を
|
|
4050
|
-
// 正確に追従でき、古い/取り違えた session_id を渡すリスクが無い。
|
|
4051
|
-
const request_id = msg.request_id
|
|
4052
|
-
const sessionName = typeof msg.session_name === "string" ? msg.session_name : ""
|
|
4053
|
-
const cwd = typeof msg.cwd === "string" ? msg.cwd : ""
|
|
4054
|
-
const cliKind = msg.cli_kind === "codex" ? "codex" : "claude"
|
|
4055
|
-
const reply = (payload) =>
|
|
4056
|
-
ctx.client.send({
|
|
4057
|
-
type: "terminal.mirror.start.result",
|
|
4058
|
-
request_id,
|
|
4059
|
-
session_name: sessionName,
|
|
4060
|
-
...payload,
|
|
4061
|
-
})
|
|
4062
|
-
if (!sessionName || !cwd) {
|
|
4063
|
-
reply({ ok: false, error: "session_name / cwd は必須です" })
|
|
4064
|
-
return
|
|
4065
|
-
}
|
|
4066
|
-
let sessionId = null
|
|
4067
|
-
if (cliKind === "codex") {
|
|
4068
|
-
for (const s of ctx.codexBridge?.sessions?.values() ?? []) {
|
|
4069
|
-
if (s.cwd === cwd && s.threadId) {
|
|
4070
|
-
sessionId = s.threadId
|
|
4071
|
-
break
|
|
4072
|
-
}
|
|
4073
|
-
}
|
|
4074
|
-
} else {
|
|
4075
|
-
sessionId = ctx.claudeBridge?.liveSessionIdForCwd?.(cwd) || null
|
|
4076
|
-
}
|
|
4077
|
-
if (!sessionId) {
|
|
4078
|
-
reply({
|
|
4079
|
-
ok: false,
|
|
4080
|
-
error: "この cwd で動作中のチャットセッションが見つかりません (先にチャットを開始してください)",
|
|
4081
|
-
})
|
|
4082
|
-
return
|
|
4083
|
-
}
|
|
4084
|
-
try {
|
|
4085
|
-
const r = await startTerminalMirror(
|
|
4086
|
-
sessionName,
|
|
4087
|
-
{ cwd, sessionId, cliKind },
|
|
4088
|
-
{ logger },
|
|
4089
|
-
)
|
|
4090
|
-
reply(r)
|
|
4091
|
-
} catch (err) {
|
|
4092
|
-
logger?.warn(
|
|
4093
|
-
{ err: err?.message, sessionName },
|
|
4094
|
-
"terminal.mirror.start failed",
|
|
4095
|
-
)
|
|
4096
|
-
reply({ ok: false, error: err?.message || String(err) })
|
|
4097
|
-
}
|
|
4098
|
-
return
|
|
4099
|
-
}
|
|
4100
|
-
case "terminal.mirror.stop": {
|
|
4101
|
-
const request_id = msg.request_id
|
|
4102
|
-
const sessionName = typeof msg.session_name === "string" ? msg.session_name : ""
|
|
4103
|
-
const reply = (payload) =>
|
|
4104
|
-
ctx.client.send({
|
|
4105
|
-
type: "terminal.mirror.stop.result",
|
|
4106
|
-
request_id,
|
|
4107
|
-
session_name: sessionName,
|
|
4108
|
-
...payload,
|
|
4109
|
-
})
|
|
4110
|
-
if (!sessionName) {
|
|
4111
|
-
reply({ ok: false, error: "session_name は必須です" })
|
|
4112
|
-
return
|
|
4113
|
-
}
|
|
4114
|
-
try {
|
|
4115
|
-
const r = await stopTerminalMirror(sessionName, { logger })
|
|
4116
|
-
reply(r)
|
|
4117
|
-
} catch (err) {
|
|
4118
|
-
logger?.warn(
|
|
4119
|
-
{ err: err?.message, sessionName },
|
|
4120
|
-
"terminal.mirror.stop failed",
|
|
4121
|
-
)
|
|
4122
|
-
reply({ ok: false, error: err?.message || String(err) })
|
|
4123
|
-
}
|
|
4124
|
-
return
|
|
4125
|
-
}
|
|
4126
4018
|
case "claude.mcp.status":
|
|
4127
4019
|
case "claude.mcp.reconnect":
|
|
4128
4020
|
case "claude.mcp.authenticate":
|
package/src/state.mjs
CHANGED
|
@@ -131,17 +131,6 @@ function stripAnsi(s) {
|
|
|
131
131
|
.replace(/\x1b[()][AB012]/g, "")
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/**
|
|
135
|
-
* capturePane 結果の末尾フッター領域 (既定 8 行) を返す。claude TUI の作業スピナー /
|
|
136
|
-
* 中断フッターはペイン最下部に固定描画されるため、本文中に同じ語が出ても誤検出しない
|
|
137
|
-
* よう、作業スピナーの補助判定はこの領域に限定する。
|
|
138
|
-
*/
|
|
139
|
-
function footerRegion(text, lines = 8) {
|
|
140
|
-
if (!text) return ""
|
|
141
|
-
const rows = text.split("\n")
|
|
142
|
-
return rows.length <= lines ? text : rows.slice(-lines).join("\n")
|
|
143
|
-
}
|
|
144
|
-
|
|
145
134
|
/**
|
|
146
135
|
* 作業スピナー行 (ライブステータス行) の構造パターン。実機採取 (2026-06-15〜):
|
|
147
136
|
* "· Blanching… (2m 4s · ↓ 7.5k tokens)" (出力中)
|
|
@@ -158,9 +147,10 @@ function footerRegion(text, lines = 8) {
|
|
|
158
147
|
* - 省略記号 (…/...) **または** 括弧タイマーのどちらか一方で可 (両方必須をやめる)
|
|
159
148
|
* - タイマーは「分のみ」「秒のみ」「分秒」いずれも可 (秒必須をやめる)
|
|
160
149
|
* - 行頭インデント制限 (\s{0,4}) を撤廃 (背の高いペイン/折返しで字下げが深い版に備える)
|
|
161
|
-
* 緩めても安全な理由: 検出は必ず
|
|
162
|
-
*
|
|
163
|
-
*
|
|
150
|
+
* 緩めても安全な理由: 検出は必ず capture 末尾 12 行への限定 (workingSpinnerLine) と
|
|
151
|
+
* _belowIsOnlyChrome (下が入力欄チロムのみ) を通すため、本文プローズ・表セル・箇条書きへの
|
|
152
|
+
* 誤点灯は下チロム判定が弾く。行頭は英数字・空白・本文記号 (-*>#|) を除く 1 文字に限定し、
|
|
153
|
+
* 語は ASCII 英字に限る (日本語プローズは非マッチ)。
|
|
164
154
|
* 完了サマリー "✻ Brewed for 2m 52s" は省略記号も**括弧**タイマーも持たない (タイマーが括弧外) ため
|
|
165
155
|
* 引き続き一致しない。
|
|
166
156
|
*/
|
|
@@ -338,7 +328,7 @@ export async function capturePane(sessionName, opts = {}) {
|
|
|
338
328
|
sessionName,
|
|
339
329
|
// 末尾 50 行を取得 (旧 30 行)。copy-mode スクロールや背の高いペインで作業/中断
|
|
340
330
|
// フッターが取得窓外に出て生成中を取りこぼす偽陰性を減らす。status 判定の作業
|
|
341
|
-
//
|
|
331
|
+
// スピナー検出はこのうち末尾 12 行 (workingSpinnerLine) に限定する。
|
|
342
332
|
"-S",
|
|
343
333
|
"-50",
|
|
344
334
|
"-E",
|