@cocorograph/hub-agent 0.6.84 → 0.6.85

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/state.mjs +55 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.6.84",
3
+ "version": "0.6.85",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/state.mjs CHANGED
@@ -132,34 +132,67 @@ function footerRegion(text, lines = 8) {
132
132
  }
133
133
 
134
134
  /**
135
- * 作業スピナーのフッター行を検出する (生成中=processing の補助シグナル)
136
- *
137
- * 現行 claude TUI の生成中フッターは語が毎ターン変わる (実機採取 2026-06-15):
135
+ * 作業スピナー行 (ライブステータス行) の構造パターン。実機採取 (2026-06-15):
138
136
  * "· Blanching… (2m 4s · ↓ 7.5k tokens)" (出力中)
139
137
  * "✽ Scurrying… (16m 55s · ↓ 66.8k tokens)" (出力中)
140
138
  * "· Puzzling… (11m 12s · thinking)" (思考中・tokens 語が無い)
141
- * (Blanching/Puzzling/Processing/Orbiting/Scurrying…) は無数にあり版で増減するため
142
- * 語リストでは判定しない。また現行版は "esc to interrupt" をフッターに出さない (主シグナル
143
- * が実機で死んでいる)。版に依存せず安定する構造は「<語>… (<ライブ経過タイマー> …)」=
144
- * 任意語 + 省略記号 + 括弧で囲まれた経過時間。完了サマリー "✻ Brewed for 2m 52s" は省略
145
- * 記号も括弧タイマーも持たないので拾わない。誤検出を抑えるため判定はフッター領域に限定する。
139
+ * = 行頭グリフ (·/✻/✽/✶… 1 文字) + 語 + 省略記号 + 括弧で囲まれたライブ経過タイマー。
140
+ * (Blanching/Puzzling/Orbiting…) は無数にあり版で増減するため語リストにしない。行頭は
141
+ * 英数字・空白・本文の箇条書き記号 (-*>#|) を除く 1 文字に限定し、本文プローズ (英字始まり)
142
+ * や箇条書きへの誤マッチを防ぐ。完了サマリー "✻ Brewed for 2m 52s" は省略記号も括弧タイマー
143
+ * も持たないので一致しない。
144
+ */
145
+ const SPINNER_LINE_RE =
146
+ /^\s{0,4}[^\sA-Za-z0-9\-*>#|]\s*[A-Za-z]+(?:…|\.\.\.)\s*\(\s*(?:\d+\s*m\s*)?\d+\s*s\b/
147
+
148
+ /**
149
+ * スピナー行 i の「下」が入力欄チロム (空行/罫線/❯ プロンプト/tips/字下げ継続/権限バナー)
150
+ * のみかを判定する。ライブのスピナーはペイン最下部の入力欄直上に描かれ、下には本文行が
151
+ * 来ない。一方、本文中に書かれた「スピナー行の引用」(解説テキスト・表セル・コードブロック等)
152
+ * は下に列 0 の本文行が続く。これを使って「未生成なのにローダーが点灯する」誤検出を防ぐ。
153
+ * 列 0 の本文行が下に在れば引用とみなし false。
154
+ */
155
+ function _belowIsOnlyChrome(lines, i) {
156
+ for (let j = i + 1; j < lines.length; j++) {
157
+ const ln = lines[j]
158
+ if (ln.trim() === "") continue // 空行
159
+ if (/^\s*─{3,}\s*$/.test(ln)) continue // 入力欄の罫線
160
+ if (/^\s*[❯>]/.test(ln)) continue // 入力プロンプト
161
+ if (/^\s*⎿/.test(ln)) continue // tips / ツリー装飾行
162
+ if (/^\s{4,}\S/.test(ln)) continue // tips 折り返し等の字下げ継続
163
+ if (
164
+ /shift\+tab|← for|for shortcuts|accept edits|plan mode|auto mode|bypass permissions|new task\?|to save|auto-compact|context left/i.test(
165
+ ln,
166
+ )
167
+ )
168
+ continue // 権限バナー / フッターのヒント
169
+ return false // 列 0 の本文行 = スピナー行の引用とみなす
170
+ }
171
+ return true
172
+ }
173
+
174
+ /**
175
+ * 作業スピナーのフッター行を検出する (生成中=processing の補助シグナル)。
146
176
  *
147
- * 2 系統で判定する:
148
- * (a) 構造シグナル: "<語>… (<m? s>" のライブタイマー。tokens の有無に依存しないので
149
- * thinking フッターも拾う (旧実装の偽陰性の根治)。tips 行 (⎿ Tip:…) でスピナー行が
150
- * 押し上げられる分を見込み少し広い領域 (12 行) を走査する。構造が限定的なので
151
- * スクロールバックの古フッター "(99s · ) old footer" 等の誤検出に強い。
152
- * (b) 旧シグナル (回帰防止): "tokens" 語 + 経過秒の同一行同居。タイマーが括弧直後でない
153
- * 語順 "(↑ 3.4k tokens · 7s)" も拾う。誤検出しやすいのでフッター 8 行に限定する。
177
+ * 現行版は "esc to interrupt" をフッターに出さない (主シグナルが実機で死んでいる) ため、
178
+ * これが生成中検出の実効シグナルになる。フッター領域 (末尾 12 行) を下から走査し、次の
179
+ * いずれかに一致しかつ「下が入力欄チロムのみ (本文行の引用でない)」行があれば生成中とみなす:
180
+ * (a) スピナー構造行 (SPINNER_LINE_RE)。tokens の有無に依存しないので thinking も拾う。
181
+ * (b) "tokens" 語 + 経過秒の同一行同居 (回帰防止。"(↑ 3.4k tokens · 7s)" の語順も拾う)。
182
+ *
183
+ * 下チロム判定 (_belowIsOnlyChrome) が肝: スピナー行の構造は、本文に「生成中フッターは
184
+ * … (Ns) です」と書いた引用や表セルにも現れ得る。それらは下に本文行が続くため除外し、
185
+ * ペイン最下部の入力欄直上にあるライブのスピナーだけを生成中と判定する (誤点灯防止)。
154
186
  */
155
187
  function detectWorkingSpinner(text) {
156
- // (a) 構造シグナル: 任意語 + 省略記号 + 括弧ライブタイマー (tokens 非依存・thinking も拾う)
157
- for (const line of footerRegion(text, 12).split("\n")) {
158
- if (/[A-Za-z]+(?:…|\.\.\.)\s*\(\s*(?:\d+\s*m\s*)?\d+\s*s\b/.test(line)) return true
159
- }
160
- // (b) 旧シグナル: tokens カウンタ + 経過秒の同一行同居 (フッター 8 行限定)
161
- for (const line of footerRegion(text, 8).split("\n")) {
162
- if (/\btokens\b/i.test(line) && /(?:^|[\s(])\d+\s*s\b/.test(line)) return true
188
+ const lines = text.split("\n")
189
+ const start = Math.max(0, lines.length - 12)
190
+ for (let i = lines.length - 1; i >= start; i--) {
191
+ const line = lines[i]
192
+ const isSpinner = SPINNER_LINE_RE.test(line)
193
+ const isTokenFooter =
194
+ /\btokens\b/i.test(line) && /(?:^|[\s(])\d+\s*s\b/.test(line)
195
+ if ((isSpinner || isTokenFooter) && _belowIsOnlyChrome(lines, i)) return true
163
196
  }
164
197
  return false
165
198
  }