@cocorograph/hub-agent 0.8.17 → 0.8.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/package.json +1 -1
- package/src/claude-md-sync.mjs +704 -57
- package/src/claude-md.mjs +12 -8
- package/src/live-sessions.mjs +61 -0
- package/src/main.mjs +187 -143
- package/src/pane-cli-flag.mjs +30 -0
- package/src/sibling-inject.mjs +21 -12
- package/src/state.mjs +39 -195
- package/src/tmux.mjs +46 -83
package/src/state.mjs
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* 等のパターンを拾う (見つからなければ null)
|
|
9
9
|
* - plugin `transformStatusDetection` hook を chain して結果差し替え可
|
|
10
10
|
*
|
|
11
|
+
* ⚠️ **既定では pane を読まない** (2026-08-01)。Cockpit のターミナルモード封印に伴い
|
|
12
|
+
* pane に CLI の TUI を起動しなくなった (`shouldLaunchPaneCli()` 既定 false) ため、
|
|
13
|
+
* capture-pane は「素のシェルの画面」を読むだけで、判定結果は退化した定値にしかならない。
|
|
14
|
+
* detectSessionState はその設定を見て capture-pane 自体をスキップする。詳細は同関数の JSDoc。
|
|
15
|
+
*
|
|
11
16
|
* 移植元: D00000_cockpit/webapp/lib/tmux.ts (getSessionStatus 部分)
|
|
12
17
|
* 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
|
|
13
18
|
*/
|
|
@@ -15,6 +20,7 @@ import { execFile } from "node:child_process"
|
|
|
15
20
|
import { promisify } from "node:util"
|
|
16
21
|
|
|
17
22
|
import { runHookChain } from "./plugin-loader.mjs"
|
|
23
|
+
import { shouldLaunchPaneCli } from "./pane-cli-flag.mjs"
|
|
18
24
|
|
|
19
25
|
const execFileP = promisify(execFile)
|
|
20
26
|
|
|
@@ -282,29 +288,6 @@ export function detectStatusFromText(text) {
|
|
|
282
288
|
return "idle"
|
|
283
289
|
}
|
|
284
290
|
|
|
285
|
-
/**
|
|
286
|
-
* 【問題1 根治 2026-06-22】bind 時の即時ステータススナップショットで配る status を、
|
|
287
|
-
* プロセス実体の busy を権威として解決する。capture (SPINNER_LINE_RE) の偽陽性に勝たせる。
|
|
288
|
-
*
|
|
289
|
-
* - busy=true (isArmed or pane_pid 子孫数 > baseline = ターン進行中): capture gap で
|
|
290
|
-
* capturedStatus が waiting に化けても processing を強制 (症状C=再マウントで生成中の三点
|
|
291
|
-
* リーダーが消える偽陰性の masking)。
|
|
292
|
-
* - busy=false (プロセス実体 idle): capturedStatus が processing でも、それは静的完了行の
|
|
293
|
-
* SPINNER_LINE_RE 偽陽性であって生成中ではないので processing を配らない (= waiting へ降格)。
|
|
294
|
-
* bind は noCache 単発読みで freeze 履歴が無く偽陽性を弾けないため、ここで busy を権威にする。
|
|
295
|
-
* 万一 no-hook セッションの無音生成中だった場合も、次の state loop tick(≤5s)が outputActive で
|
|
296
|
-
* processing に復帰させるので実害は限定的。
|
|
297
|
-
*
|
|
298
|
-
* @param {boolean} busy プロセス実体の busy (isArmed or 子孫数>baseline)
|
|
299
|
-
* @param {string} capturedStatus detectSessionState の status
|
|
300
|
-
* @returns {string}
|
|
301
|
-
*/
|
|
302
|
-
export function resolveBindSnapshotStatus(busy, capturedStatus) {
|
|
303
|
-
if (busy) return "processing"
|
|
304
|
-
if (capturedStatus === "processing") return "waiting"
|
|
305
|
-
return capturedStatus
|
|
306
|
-
}
|
|
307
|
-
|
|
308
291
|
export function detectContextPctFromText(text) {
|
|
309
292
|
for (const re of CONTEXT_PATTERNS) {
|
|
310
293
|
const m = text.match(re)
|
|
@@ -326,177 +309,6 @@ export function detectContextPctFromText(text) {
|
|
|
326
309
|
return null
|
|
327
310
|
}
|
|
328
311
|
|
|
329
|
-
// 対話 claude TUI のフッターに出る権限モードバナー (claude v2.1 系で実機確認)。
|
|
330
|
-
// shift+tab で循環: default(バナー無し) → accept edits → plan → auto → …
|
|
331
|
-
const PERMISSION_BANNERS = [
|
|
332
|
-
[/⏵⏵\s*accept edits on/i, "acceptEdits"],
|
|
333
|
-
[/⏸\s*plan mode on/i, "plan"],
|
|
334
|
-
[/⏵⏵\s*auto mode on/i, "auto"],
|
|
335
|
-
[/bypass permissions on/i, "bypassPermissions"],
|
|
336
|
-
]
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* 対話 claude TUI のペインテキストから現在の権限モードを判定する。
|
|
340
|
-
*
|
|
341
|
-
* 全端末で「実際に動いているターミナルの権限状態」を同期するための正本。
|
|
342
|
-
* jsonl の permissionMode は送信ターン毎にしか記録されないため、shift+tab した
|
|
343
|
-
* だけ (未送信) の変更や tmux 上で直接切り替えた変更は jsonl に出ない。ペインの
|
|
344
|
-
* フッターバナーは shift+tab 直後に即更新されるので、これを正本にする。
|
|
345
|
-
*
|
|
346
|
-
* - 明示バナー (accept edits / plan / auto / bypass) があればそのモード。
|
|
347
|
-
* - バナーが無い場合、claude TUI のフッターが見えている時だけ "default" と判定する
|
|
348
|
-
* (素のシェルを default と誤検出しないためのガード)。
|
|
349
|
-
* - claude TUI と判別できなければ null (= 不明、呼び出し側は据え置き)。
|
|
350
|
-
*
|
|
351
|
-
* @param {string} text capturePane の結果 (ANSI 除去済み)
|
|
352
|
-
* @returns {string|null}
|
|
353
|
-
*/
|
|
354
|
-
export function detectPermissionModeFromText(text) {
|
|
355
|
-
if (!text) return null
|
|
356
|
-
for (const [re, mode] of PERMISSION_BANNERS) {
|
|
357
|
-
if (re.test(text)) return mode
|
|
358
|
-
}
|
|
359
|
-
// バナー無し = default。ただし claude TUI のフッターが見えている時だけ assert する。
|
|
360
|
-
if (/← for agents|for shortcuts|shift\+tab to cycle/i.test(text)) {
|
|
361
|
-
return "default"
|
|
362
|
-
}
|
|
363
|
-
return null
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* 対話 claude TUI のペインテキストから「入力欄内の本文」を取り出す。
|
|
368
|
-
*
|
|
369
|
-
* 中断キャンセル後の入力欄復旧 (claude.tui.recoverInput) の読込側。claude は
|
|
370
|
-
* 「送信直後の Esc」で直前メッセージの本文を入力欄へ戻すため、これを実測して
|
|
371
|
-
* クリアしないと、次の bracketed paste が後ろへ連結されてメッセージが合体する。
|
|
372
|
-
*
|
|
373
|
-
* 2 形式に対応する (claude v2.1.16x 実機確認 2026-06-08):
|
|
374
|
-
* - **現行 (ルール区切り)**: `───…` / `❯ 本文` (折り返しは 2 スペース字下げで続行) /
|
|
375
|
-
* `───…` / フッター。入力プロンプト `❯` は列 0 から始まる (選択ダイアログのカーソル
|
|
376
|
-
* ` ❯ 1. …` は字下げされるため誤検出しない)。
|
|
377
|
-
* - **旧 (罫線ボックス)**: `╭─…─╮ / │ > 本文 │ / ╰─…─╯`。末尾から最も近い枠の先頭行が
|
|
378
|
-
* プロンプト (`>` / `❯`) で始まる時だけ採用する。
|
|
379
|
-
*
|
|
380
|
-
* 空入力時のプレースホルダ等も文字列として返り得るので、呼び出し側は「期待する本文と
|
|
381
|
-
* 一致するか」で判定すること (本関数は在中テキストの抽出のみを担う)。
|
|
382
|
-
*
|
|
383
|
-
* @param {string} text capturePane の結果 (ANSI 除去済み)
|
|
384
|
-
* @returns {string|null} 入力欄内の本文 (入力欄が見つからない / 空なら null)
|
|
385
|
-
*/
|
|
386
|
-
/**
|
|
387
|
-
* 現行形式 (ルール区切り) の入力ボックス本文を「上下ボーダー間」から抽出する。
|
|
388
|
-
*
|
|
389
|
-
* 末尾から `❯` を逆走する旧方式は、ユーザー評価プロンプト ("How is Claude doing?") や
|
|
390
|
-
* 選択ダイアログの選択肢行 (`❯ Very good` 等) の ❯ に誤マッチし得た。評価プロンプトは
|
|
391
|
-
* 入力欄の「上」に出るため入力欄自体の上下ボーダーはズレない、という知見に基づき、
|
|
392
|
-
* 下ボーダー (ペイン末尾近傍の ────) → 直上の上ボーダー → 間の ❯ プロンプト行、という
|
|
393
|
-
* 範囲ベースで確定する。誤認防止ガード: 下ボーダーはペイン末尾から MAX_FOOTER_LINES 行
|
|
394
|
-
* 以内、上下ボーダー間は 2〜MAX_BOX_LINES 行、間に列 0 の ❯ が在ること。
|
|
395
|
-
* 見つからなければ null を返し、呼び出し側は旧アンカー方式へフォールバックする。
|
|
396
|
-
*
|
|
397
|
-
* @param {string} text capturePane の結果 (ANSI 除去済み)
|
|
398
|
-
* @returns {string|null}
|
|
399
|
-
*/
|
|
400
|
-
function _inputTextFromRuleBox(text) {
|
|
401
|
-
if (!text) return null
|
|
402
|
-
const lines = text.split("\n")
|
|
403
|
-
const isRule = (s) => /^\s*─{4,}\s*$/.test(s)
|
|
404
|
-
const MAX_FOOTER_LINES = 6 // 入力欄の下に出るヒント/ナビ行の想定上限
|
|
405
|
-
const MAX_BOX_LINES = 50 // 上下ボーダー間の許容行数 (折り返し長文入力の上限)
|
|
406
|
-
// 下ボーダー: ペイン末尾近傍 (下に出るヒント行のすぐ上) の ──── を末尾から探す。
|
|
407
|
-
let bottom = -1
|
|
408
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
409
|
-
if (lines.length - 1 - i > MAX_FOOTER_LINES) break
|
|
410
|
-
if (isRule(lines[i])) {
|
|
411
|
-
bottom = i
|
|
412
|
-
break
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
if (bottom < 0) return null
|
|
416
|
-
// 上ボーダー: 下ボーダーの直上にある ──── 。
|
|
417
|
-
let top = -1
|
|
418
|
-
for (let i = bottom - 1; i >= 0 && bottom - i <= MAX_BOX_LINES; i--) {
|
|
419
|
-
if (isRule(lines[i])) {
|
|
420
|
-
top = i
|
|
421
|
-
break
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
if (top < 0 || bottom - top < 2) return null
|
|
425
|
-
// ボーダー間に列 0 の ❯ プロンプト行が在ること (選択ダイアログの字下げ ❯ は対象外)。
|
|
426
|
-
const inner = lines.slice(top + 1, bottom)
|
|
427
|
-
const promptIdx = inner.findIndex((ln) => /^❯/.test(ln))
|
|
428
|
-
if (promptIdx < 0) return null
|
|
429
|
-
const parts = [inner[promptIdx].replace(/^❯\s?/, "").replace(/\s+$/, "")]
|
|
430
|
-
for (let i = promptIdx + 1; i < inner.length; i++) {
|
|
431
|
-
parts.push(inner[i].replace(/\s+$/, ""))
|
|
432
|
-
}
|
|
433
|
-
const joined = parts.join("\n").trim()
|
|
434
|
-
return joined || null
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
export function detectInputBoxText(text) {
|
|
438
|
-
// 第一候補: 上下ボーダー間抽出 (評価プロンプト/選択ダイアログの ❯ 誤マッチに強い)。
|
|
439
|
-
const ruled = _inputTextFromRuleBox(text)
|
|
440
|
-
if (ruled !== null) return ruled
|
|
441
|
-
// フォールバック: 旧 ❯ アンカー / 罫線ボックス方式 (ボーダー未描画版・レイアウト差用)。
|
|
442
|
-
return _inputTextFromPromptAnchor(text)
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function _inputTextFromPromptAnchor(text) {
|
|
446
|
-
if (!text) return null
|
|
447
|
-
const lines = text.split("\n")
|
|
448
|
-
|
|
449
|
-
// ── 現行形式: 列 0 の `❯` プロンプト行 + 下側ルールまでの続行行 ──
|
|
450
|
-
let promptIdx = -1
|
|
451
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
452
|
-
if (/^❯/.test(lines[i])) {
|
|
453
|
-
promptIdx = i
|
|
454
|
-
break
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
if (promptIdx >= 0) {
|
|
458
|
-
const parts = [lines[promptIdx].replace(/^❯\s?/, "").replace(/\s+$/, "")]
|
|
459
|
-
for (let i = promptIdx + 1; i < lines.length; i++) {
|
|
460
|
-
const ln = lines[i]
|
|
461
|
-
if (/^\s*─{4,}\s*$/.test(ln)) break // 下側ルールで入力欄が終わる
|
|
462
|
-
parts.push(ln.replace(/\s+$/, ""))
|
|
463
|
-
}
|
|
464
|
-
const joined = parts.join("\n").trim()
|
|
465
|
-
return joined || null
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// ── 旧形式: 罫線ボックス ──
|
|
469
|
-
let bottom = -1
|
|
470
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
471
|
-
if (/^\s*╰/.test(lines[i])) {
|
|
472
|
-
bottom = i
|
|
473
|
-
break
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
if (bottom < 0) return null
|
|
477
|
-
let top = -1
|
|
478
|
-
for (let i = bottom - 1; i >= 0; i--) {
|
|
479
|
-
if (/^\s*╭/.test(lines[i])) {
|
|
480
|
-
top = i
|
|
481
|
-
break
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
if (top < 0 || bottom - top < 2) return null
|
|
485
|
-
const inner = lines.slice(top + 1, bottom)
|
|
486
|
-
const first = inner[0].match(/^\s*│\s*[>❯]\s?(.*)$/)
|
|
487
|
-
if (!first) return null
|
|
488
|
-
// 右罫線と行末空白を落とす (折り返し行は左罫線 + 字下げで続く)。
|
|
489
|
-
const strip = (s) => s.replace(/\s*│\s*$/, "").replace(/\s+$/, "")
|
|
490
|
-
const parts = [strip(first[1])]
|
|
491
|
-
for (let i = 1; i < inner.length; i++) {
|
|
492
|
-
const m = inner[i].match(/^\s*│\s?(.*)$/)
|
|
493
|
-
if (!m) break
|
|
494
|
-
parts.push(strip(m[1]))
|
|
495
|
-
}
|
|
496
|
-
const joined = parts.join("\n").trim()
|
|
497
|
-
return joined || null
|
|
498
|
-
}
|
|
499
|
-
|
|
500
312
|
export async function listSessionNames(opts = {}) {
|
|
501
313
|
const tmuxBin = opts.tmuxBin || "tmux"
|
|
502
314
|
try {
|
|
@@ -622,7 +434,36 @@ function _computeStable(sessionName, status, text) {
|
|
|
622
434
|
return now - prev.since >= STABLE_CONFIRM_MS
|
|
623
435
|
}
|
|
624
436
|
|
|
437
|
+
/**
|
|
438
|
+
* pane に CLI を起動していないときの退化後の定値。
|
|
439
|
+
*
|
|
440
|
+
* pane が素のシェルなら capture-pane を読んでも:
|
|
441
|
+
* status … スピナーも "esc to interrupt" も ❯ プロンプトも出ないので常に 'idle'
|
|
442
|
+
* context_pct … "X% context left" は TUI のフッターにしか出ないので常に null
|
|
443
|
+
* stable … 画面が動かないので常に true
|
|
444
|
+
* spinner_present … 常に false
|
|
445
|
+
* output_sig … 不変なので出力フロー検知の情報量がゼロ
|
|
446
|
+
* にしかならない。読む価値がゼロの subprocess を全セッション × 5s 周期で fork し続ける
|
|
447
|
+
* ことになるため、この場合は capture 自体を行わずこの定値を返す。
|
|
448
|
+
*
|
|
449
|
+
* ⚠️ output_sig は「読んでいない」を表す null。空文字の署名 (`_outputSignature("")`) を
|
|
450
|
+
* 返すと main.mjs の出力フロー検知が「不変な署名を観測し続けている」と誤読するため、
|
|
451
|
+
* 型で区別できる null を返して当該経路をスキップさせる。
|
|
452
|
+
*/
|
|
453
|
+
const PANE_UNREAD_STATE = Object.freeze({
|
|
454
|
+
status: "idle",
|
|
455
|
+
context_pct: null,
|
|
456
|
+
permission_mode: null,
|
|
457
|
+
stable: true,
|
|
458
|
+
spinner_present: false,
|
|
459
|
+
output_sig: null,
|
|
460
|
+
})
|
|
461
|
+
|
|
625
462
|
export async function detectSessionState(sessionName, opts = {}) {
|
|
463
|
+
// ペインに CLI の TUI を起動していない構成 (既定) では、capture-pane は素のシェルの
|
|
464
|
+
// 画面を読むだけで何も供給しない。subprocess を焚く前にここで打ち切る。
|
|
465
|
+
// `HUB_AGENT_TMUX_LAUNCH_CLI=1` の逃げ道 (pane に TUI を起動する) では従来どおり読む。
|
|
466
|
+
if (!shouldLaunchPaneCli()) return { ...PANE_UNREAD_STATE }
|
|
626
467
|
const text = await capturePane(sessionName, opts)
|
|
627
468
|
let defaultStatus = detectStatusFromText(text)
|
|
628
469
|
// 凍結スピナー対策: スピナーが画面に在るが経過タイマーが進んでいない (claude 異常終了/ハング)
|
|
@@ -637,7 +478,10 @@ export async function detectSessionState(sessionName, opts = {}) {
|
|
|
637
478
|
/❯\s/.test(text) || /^>\s/m.test(text) ? "waiting" : "idle"
|
|
638
479
|
}
|
|
639
480
|
const defaultContextPct = detectContextPctFromText(text)
|
|
640
|
-
|
|
481
|
+
// permission_mode はペインのフッターバナーから読んでいたが、消費者がゼロ (frontend の
|
|
482
|
+
// SessionStateRow に該当フィールドが無い) なので判定ごと撤去した。返り値のキーだけは
|
|
483
|
+
// plugin hook (transformStatusDetection) が差し替えられる契約として残す。
|
|
484
|
+
const defaultPermissionMode = null
|
|
641
485
|
// スピナー行 (ライブ or 凍結) が画面に在るか。frozen 降格で status が waiting に落ちても、
|
|
642
486
|
// スピナーが画面に残っている = ターンはクリーンに終わっていない (ハング候補)。StallTracker が
|
|
643
487
|
// 「クリーン idle (スピナー無し) でターンを閉じる」判定に使い、frozen スピナーのハングを拾う。
|
package/src/tmux.mjs
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* - `listSessions()`: 構造化された TmuxSession[] を返す。各 session に
|
|
6
6
|
* status (state.mjs の detectSessionState) と cwd (active pane の current_path)
|
|
7
7
|
* を並列で付与
|
|
8
|
-
* - `createSession(name, cwd, opts)`: detached new-session
|
|
9
|
-
*
|
|
8
|
+
* - `createSession(name, cwd, opts)`: detached new-session。既定では pane に CLI
|
|
9
|
+
* (claude / codex) を起動せず素のシェルのまま置く。`HUB_AGENT_TMUX_LAUNCH_CLI=1`
|
|
10
|
+
* のときだけ従来どおり CLAUDE_CMD / CODEX_CMD を send-keys する。既存ならエラー throw
|
|
10
11
|
* - `killSession(name)`: 1 session を kill。存在しない場合は 'session not found' throw
|
|
11
12
|
* - `killManySessions(names)`: 複数 kill、存在しないものは無視
|
|
12
13
|
*
|
|
@@ -22,12 +23,8 @@ import { fileURLToPath } from "node:url"
|
|
|
22
23
|
|
|
23
24
|
import { ensureClaudeMd } from "./claude-md.mjs"
|
|
24
25
|
import { syncClaudeMdWithHub } from "./claude-md-sync.mjs"
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
detectInputBoxText,
|
|
28
|
-
detectSessionState,
|
|
29
|
-
getSessionCwd,
|
|
30
|
-
} from "./state.mjs"
|
|
26
|
+
import { detectSessionState, getSessionCwd } from "./state.mjs"
|
|
27
|
+
import { shouldLaunchPaneCli } from "./pane-cli-flag.mjs"
|
|
31
28
|
import { baseSessionName, sessionNameFor } from "./session-names.mjs"
|
|
32
29
|
import { boundSessionId, getSessionUsages, isFreshUnboundBind, jsonlContextForCwd } from "./usage.mjs"
|
|
33
30
|
|
|
@@ -509,6 +506,10 @@ export function composeClaudeCmdFromOptions(opts = {}) {
|
|
|
509
506
|
|
|
510
507
|
const DEFAULT_CLAUDE_CMD = buildClaudeCmd()
|
|
511
508
|
|
|
509
|
+
// 判定の実体は ./pane-cli-flag.mjs (state.mjs との循環 import を避けるため分離)。
|
|
510
|
+
// ここからの re-export は後方互換 (既存の `from "./tmux.mjs"` を壊さない)。
|
|
511
|
+
export { shouldLaunchPaneCli }
|
|
512
|
+
|
|
512
513
|
function tmuxBin(opts = {}) {
|
|
513
514
|
return opts.tmuxBin || DEFAULT_TMUX_BIN
|
|
514
515
|
}
|
|
@@ -546,6 +547,10 @@ export function injectInitialPrompt(claudeCmd, initialPrompt) {
|
|
|
546
547
|
* しか効かないため、injectInitialPrompt と同様に `||` で split して両側に付ける。
|
|
547
548
|
*
|
|
548
549
|
* configDir が空 (= 既定プロファイル) のときは何もしない。
|
|
550
|
+
*
|
|
551
|
+
* ⚠️ この関数は **CLI を pane 起動するときだけ** 使われる
|
|
552
|
+
* (`HUB_AGENT_TMUX_LAUNCH_CLI=1`)。素のシェルの pane へは setTmuxGlobalEnv の
|
|
553
|
+
* `set-environment -g` 経由で CLAUDE_CONFIG_DIR が渡る。
|
|
549
554
|
*/
|
|
550
555
|
export function injectConfigDirEnv(claudeCmd, configDir) {
|
|
551
556
|
if (!configDir || typeof configDir !== "string") return claudeCmd
|
|
@@ -559,8 +564,10 @@ export function injectConfigDirEnv(claudeCmd, configDir) {
|
|
|
559
564
|
|
|
560
565
|
/**
|
|
561
566
|
* tmux サーバーのグローバル環境変数を設定/解除する。
|
|
562
|
-
*
|
|
563
|
-
*
|
|
567
|
+
* pane は既定で素のシェルなので、ユーザーが pane 内で手動起動する claude / codex に
|
|
568
|
+
* CLAUDE_CONFIG_DIR (CODEX_HOME) を届ける **主経路** がこれ。
|
|
569
|
+
* (CLI を pane 起動する `HUB_AGENT_TMUX_LAUNCH_CLI=1` のときだけ、
|
|
570
|
+
* injectConfigDirEnv のコマンド焼き込みが併用される)。
|
|
564
571
|
* value が null/undefined なら unset (-gu)。tmux サーバー未起動などの失敗は握り潰す。
|
|
565
572
|
*/
|
|
566
573
|
export async function setTmuxGlobalEnv(name, value, opts = {}) {
|
|
@@ -766,12 +773,21 @@ export async function killManySessions(names, opts = {}) {
|
|
|
766
773
|
}
|
|
767
774
|
|
|
768
775
|
/**
|
|
769
|
-
* 新規 session を detached
|
|
776
|
+
* 新規 session を detached で作成する。
|
|
777
|
+
*
|
|
778
|
+
* ⚠️ **既定では pane に CLI (claude / codex) を起動しない**(pane は素のシェルのまま)。
|
|
779
|
+
* Cockpit の通常運用はチャットモードで、pane の TUI は誰も見ないまま常駐して
|
|
780
|
+
* メモリを食うだけだったため (詳細は `pane-cli-flag.mjs` の shouldLaunchPaneCli の JSDoc)。
|
|
781
|
+
* `HUB_AGENT_TMUX_LAUNCH_CLI=1` のときだけ従来どおり CLI を send-keys する。
|
|
782
|
+
*
|
|
770
783
|
* - 同名 session が既にあれば 'duplicate session' throw
|
|
771
|
-
* -
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
784
|
+
* - CLAUDE.md / AGENTS.md の生成・Hub 同期、cwd 自動作成、mouse mode 設定は
|
|
785
|
+
* **CLI を起動するかどうかに関わらず**常に行う(素のシェルでも必要なため)
|
|
786
|
+
* - 以下は CLI を起動するときだけ意味を持つので、起動時のみ評価される:
|
|
787
|
+
* opts.claudeCmd / opts.codexCmd / opts.cliKind / opts.claudeConfigDir /
|
|
788
|
+
* opts.codexHome / opts.initialPrompt / model・sandbox 等の codex フラグ
|
|
789
|
+
* - opts.initialPrompt は CLI 起動時に位置引数として末尾へ shell-safe に埋め込まれ、
|
|
790
|
+
* TUI の初回プロンプトとして実行される (`/orchestrate` 等のスキル発火に利用)
|
|
775
791
|
*/
|
|
776
792
|
export async function createSession(name, cwd, opts = {}) {
|
|
777
793
|
// tmux は `-c` をシェル展開しないため、ここで `~` を絶対パスに展開する。
|
|
@@ -918,6 +934,21 @@ export async function createSession(name, cwd, opts = {}) {
|
|
|
918
934
|
// display-message 失敗は致命的でないので飲み込む
|
|
919
935
|
}
|
|
920
936
|
}
|
|
937
|
+
// 既定では pane を素のシェルのまま残す (CLI は起動しない)。ここから下は
|
|
938
|
+
// HUB_AGENT_TMUX_LAUNCH_CLI=1 のときだけ走る。
|
|
939
|
+
//
|
|
940
|
+
// 素のシェルでも必要な処理 (cwd 解決・CLAUDE.md 生成/同期・mouse mode) は上で
|
|
941
|
+
// 済ませてある。CLAUDE_CONFIG_DIR / CODEX_HOME も、pane で手動起動したときに
|
|
942
|
+
// 効くよう main.mjs の syncTmuxProfileEnv / syncTmuxCodexEnv が
|
|
943
|
+
// `tmux set-environment -g` でサーバーのグローバル環境へ流し込んでいるため、
|
|
944
|
+
// ここでコマンド文字列に焼き込む必要があるのは CLI を起動する場合だけ。
|
|
945
|
+
if (!shouldLaunchPaneCli()) {
|
|
946
|
+
opts.logger?.debug?.(
|
|
947
|
+
{ session: name, cwd: resolvedCwd },
|
|
948
|
+
"pane CLI launch skipped (set HUB_AGENT_TMUX_LAUNCH_CLI=1 to enable)",
|
|
949
|
+
)
|
|
950
|
+
return
|
|
951
|
+
}
|
|
921
952
|
// cliKind に応じて claude / codex いずれの CLI を pane で起動するか分岐する。
|
|
922
953
|
// 既定は claude(後方互換)。initialPrompt はどちらも位置引数として末尾注入する
|
|
923
954
|
// (claude "<p>" / codex "<p>" / codex resume <id> "<p>" いずれも [PROMPT] を受ける)。
|
|
@@ -1114,74 +1145,6 @@ export function injectCodexHomeEnv(codexCmd, codexHome) {
|
|
|
1114
1145
|
|
|
1115
1146
|
const _delay = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
1116
1147
|
|
|
1117
|
-
/**
|
|
1118
|
-
* `/model` / `/effort` 切替時に claude TUI が出す確認ダイアログを検知する正規表現。
|
|
1119
|
-
*
|
|
1120
|
-
* claude v2.1.170 実機確認 (2026-06-10): 会話に出力済みの履歴 (prior output) があると、
|
|
1121
|
-
* モデル/effort 切替は即時反映されず確認ダイアログを挟む。ダイアログは:
|
|
1122
|
-
* - タイトル: "Switch model?" / "Change effort level?"
|
|
1123
|
-
* - 本文: "This conversation is cached for the current model. Switching to … means the
|
|
1124
|
-
* full history gets re-read on your next message."
|
|
1125
|
-
* - 選択肢: "Yes, switch to …" (デフォルトハイライト) / "No, go back"
|
|
1126
|
-
* デフォルトが確定側なので Enter 1 回で承認できる。複数マーカーのいずれかで判定する
|
|
1127
|
-
* (文言の小変更に多少強くする)。
|
|
1128
|
-
*/
|
|
1129
|
-
const SWITCH_CONFIRM_DIALOG_RE =
|
|
1130
|
-
/Switch model\?|Change effort level\?|Yes, switch to|No, go back|full history gets re-read/i
|
|
1131
|
-
|
|
1132
|
-
/**
|
|
1133
|
-
* `/model` / `/effort` の確認ダイアログを「描画を検知してから」Enter で畳む。
|
|
1134
|
-
*
|
|
1135
|
-
* 旧実装は本文確定 Enter の一定時間後に盲打ちで追い Enter を 1 回送っていたが、確認
|
|
1136
|
-
* ダイアログのレンダはコマンド確定から遅延することがあり (TUI の全画面再描画ラグ)、
|
|
1137
|
-
* 固定待ちの Enter がダイアログ描画前に着弾して取りこぼされ、ダイアログが残ったまま
|
|
1138
|
-
* 次の入力の Enter を吸収する事故になっていた。代わりにペインを短周期で読み、ダイアログ
|
|
1139
|
-
* が実際に描画されたのを確認してから Enter を送り、消えるまで数回リトライする。ダイアログ
|
|
1140
|
-
* が出ない場合 (prior output 無し = 即時反映) は余計な Enter を送らない (空 Enter が
|
|
1141
|
-
* 別の場面で誤確定するのを避ける)。ベストエフォート。
|
|
1142
|
-
*
|
|
1143
|
-
* タイミングは opts で上書きできる (テストで短縮するため):
|
|
1144
|
-
* - confirmAppearMs: ダイアログ出現を待つ上限 (既定 2400ms)
|
|
1145
|
-
* - confirmPollMs: 出現待ちのポーリング間隔 (既定 150ms)
|
|
1146
|
-
* - confirmDismissMs: 承認後にダイアログ消失を待つ上限 (既定 2000ms)
|
|
1147
|
-
*
|
|
1148
|
-
* @param {string} bin tmux バイナリパス
|
|
1149
|
-
* @param {string} name tmux セッション名
|
|
1150
|
-
* @param {{logger?:object,tmuxBin?:string,confirmAppearMs?:number,confirmPollMs?:number,confirmDismissMs?:number}} opts
|
|
1151
|
-
* @returns {Promise<boolean>} 確認ダイアログを承認したら true
|
|
1152
|
-
*/
|
|
1153
|
-
async function _confirmSwitchDialog(bin, name, opts) {
|
|
1154
|
-
const appearMs = opts.confirmAppearMs ?? 2400
|
|
1155
|
-
const pollMs = opts.confirmPollMs ?? 150
|
|
1156
|
-
const dismissMs = opts.confirmDismissMs ?? 2000
|
|
1157
|
-
// ダイアログ出現待ち: コマンド確定から最大 appearMs 観測する (レンダ遅延の上限想定)。
|
|
1158
|
-
const APPEAR_DEADLINE = Date.now() + appearMs
|
|
1159
|
-
let appeared = false
|
|
1160
|
-
while (Date.now() < APPEAR_DEADLINE) {
|
|
1161
|
-
const text = await capturePane(name, { ...opts, noCache: true })
|
|
1162
|
-
if (SWITCH_CONFIRM_DIALOG_RE.test(text)) {
|
|
1163
|
-
appeared = true
|
|
1164
|
-
break
|
|
1165
|
-
}
|
|
1166
|
-
await _delay(pollMs)
|
|
1167
|
-
}
|
|
1168
|
-
if (!appeared) return false // prior output 無し = 即時反映済み。追い Enter 不要。
|
|
1169
|
-
|
|
1170
|
-
// ダイアログが消える (= 承認反映) まで Enter を送ってリトライする。
|
|
1171
|
-
const DISMISS_DEADLINE = Date.now() + dismissMs
|
|
1172
|
-
while (Date.now() < DISMISS_DEADLINE) {
|
|
1173
|
-
await execFileP(bin, ["send-keys", "-t", name, "Enter"])
|
|
1174
|
-
await _delay(180)
|
|
1175
|
-
const after = await capturePane(name, { ...opts, noCache: true })
|
|
1176
|
-
if (!SWITCH_CONFIRM_DIALOG_RE.test(after)) return true
|
|
1177
|
-
}
|
|
1178
|
-
opts.logger?.warn(
|
|
1179
|
-
{ session: name },
|
|
1180
|
-
"switch confirm dialog still visible after retries",
|
|
1181
|
-
)
|
|
1182
|
-
return true
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
1148
|
/**
|
|
1186
1149
|
* 離席中キュー投入: 送信待ちメッセージ 1 件を、ブラウザがそのセッションのターミナルを
|
|
1187
1150
|
* マウントしていなくても tmux send-keys で対話 claude TUI へ投入する (案A の agent 側実体)。
|