@cocorograph/hub-agent 0.6.48 → 0.6.50
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-history.mjs +34 -0
- package/src/main.mjs +145 -4
- package/src/state.mjs +45 -1
- package/src/tmux.mjs +47 -0
package/package.json
CHANGED
package/src/claude-history.mjs
CHANGED
|
@@ -241,3 +241,37 @@ export async function listSessions({ cwd, projectsRoot, limit = 30, logger }) {
|
|
|
241
241
|
}
|
|
242
242
|
return { sessions }
|
|
243
243
|
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* TUI チャット閲覧中に裏の対話 claude が新しい session_id へ「回転」したか
|
|
247
|
+
* (= `/clear` で新 jsonl が生まれたか) を判定する純関数。
|
|
248
|
+
*
|
|
249
|
+
* 背景: TUI チャットは bind 時に決めた session_id を固定して jsonl を読む。ユーザーが
|
|
250
|
+
* `/clear` すると裏の claude は**新しい session_id (新 jsonl)** に切り替わるが、フロントは
|
|
251
|
+
* 旧 jsonl を読み続けるため「読み出し先が切り替わらない」「生成中で固まる」「新セッションへの
|
|
252
|
+
* 送信が表示されない」という不整合が起きる (T04786 後続調査)。これを agent 側で検知して
|
|
253
|
+
* `claude.session.rotated` を push し、フロントに読み出し先の切替を促すための判定ロジック。
|
|
254
|
+
*
|
|
255
|
+
* 判定材料:
|
|
256
|
+
* - `viewingSessionId`: フロントが今表示している (閲覧ハートビートが運ぶ) session_id。
|
|
257
|
+
* - `newestSessionId`: cwd 配下で実際に最新 (mtime 降順先頭) の session_id。
|
|
258
|
+
* - `lastNotifiedNewId`: 同一 new への多重通知を防ぐため、最後に通知した new_session_id。
|
|
259
|
+
*
|
|
260
|
+
* 回転とみなす条件: 最新が存在し、閲覧中 id と異なり、かつ直近で同じ new を通知済みでない。
|
|
261
|
+
* 注意: 過去セッションを意図的に開いている (= newest 非追従) ビューでは呼び出し側が
|
|
262
|
+
* `follow_newest=false` でこの判定自体をスキップすること (ピン留め閲覧を勝手に最新へ
|
|
263
|
+
* 引きずらないため)。本関数は「追従中ビュー」前提で newest とのズレだけを見る。
|
|
264
|
+
*
|
|
265
|
+
* @param {{viewingSessionId?: string|null, newestSessionId?: string|null, lastNotifiedNewId?: string|null}} args
|
|
266
|
+
* @returns {{rotated: boolean, newSessionId?: string}}
|
|
267
|
+
*/
|
|
268
|
+
export function decideSessionRotation({
|
|
269
|
+
viewingSessionId,
|
|
270
|
+
newestSessionId,
|
|
271
|
+
lastNotifiedNewId,
|
|
272
|
+
} = {}) {
|
|
273
|
+
if (!newestSessionId || !viewingSessionId) return { rotated: false }
|
|
274
|
+
if (newestSessionId === viewingSessionId) return { rotated: false }
|
|
275
|
+
if (newestSessionId === lastNotifiedNewId) return { rotated: false }
|
|
276
|
+
return { rotated: true, newSessionId: newestSessionId }
|
|
277
|
+
}
|
package/src/main.mjs
CHANGED
|
@@ -24,10 +24,18 @@ import { WsClient } from "./ws-client.mjs"
|
|
|
24
24
|
import { PtyBridge } from "./pty-bridge.mjs"
|
|
25
25
|
import { ClaudeStreamBridge } from "./claude-stream-bridge.mjs"
|
|
26
26
|
import { UploadManager } from "./claude-upload.mjs"
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
decideSessionRotation,
|
|
29
|
+
fetchSessionHistory,
|
|
30
|
+
listSessions,
|
|
31
|
+
} from "./claude-history.mjs"
|
|
28
32
|
import { listAgents } from "./agents.mjs"
|
|
29
33
|
import { listSkills } from "./skills.mjs"
|
|
30
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
capturePane,
|
|
36
|
+
detectPermissionModeFromText,
|
|
37
|
+
listSessionStates,
|
|
38
|
+
} from "./state.mjs"
|
|
31
39
|
import {
|
|
32
40
|
DEFAULT_PROFILE_ID,
|
|
33
41
|
defaultConfigDir,
|
|
@@ -41,6 +49,7 @@ import {
|
|
|
41
49
|
buildClaudeCmd,
|
|
42
50
|
createSession as createTmuxSession,
|
|
43
51
|
createWorktreeDir,
|
|
52
|
+
cyclePermissionMode,
|
|
44
53
|
execTmux,
|
|
45
54
|
killManySessions,
|
|
46
55
|
killSession as killTmuxSession,
|
|
@@ -457,6 +466,12 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
457
466
|
// 同じ id への再 respawn (= claude 再起動) を抑止する。
|
|
458
467
|
ctx.tuiReboundSessions = new Map()
|
|
459
468
|
|
|
469
|
+
// session 回転通知 (T04786 後続) の多重送信ガード: 回転キー (tmux 名 or cwd) →
|
|
470
|
+
// 最後に push した new_session_id。閲覧ハートビートは 5s 間隔で来るため、フロントが
|
|
471
|
+
// 切替を完了し new id でハートビートを送り直すまでの間に来る余分なハートビートで
|
|
472
|
+
// claude.session.rotated を連発しないようにする。
|
|
473
|
+
ctx.tuiRotationNotified = new Map()
|
|
474
|
+
|
|
460
475
|
const shutdown = async (signal) => {
|
|
461
476
|
logger.info({ signal }, "shutting down")
|
|
462
477
|
await runHookBroadcast(plugins, "onAgentStop", ctx)
|
|
@@ -701,6 +716,9 @@ function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge }) {
|
|
|
701
716
|
for (const s of states) {
|
|
702
717
|
let status = s.status
|
|
703
718
|
let contextPct = s.context_pct
|
|
719
|
+
// 対話 TUI のペインから読んだ権限モード (素のシェル / SDK チャットでは null)。
|
|
720
|
+
// tmux 上で直接 shift+tab した変更もここで拾い、全ブラウザへ追従させる。
|
|
721
|
+
const permissionMode = s.permission_mode ?? null
|
|
704
722
|
// チャットモード補完: tmux ペインのスクレイプは TUI 前提で効かないため、
|
|
705
723
|
// session の cwd に一致する新鮮なチャット信号があれば status/context% を
|
|
706
724
|
// 上書きする (Hub ホスト型 Cockpit のステータスドット/各行ドーナツ用)。
|
|
@@ -728,17 +746,20 @@ function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge }) {
|
|
|
728
746
|
if (
|
|
729
747
|
!prev ||
|
|
730
748
|
prev.status !== status ||
|
|
731
|
-
prev.context_pct !== contextPct
|
|
749
|
+
prev.context_pct !== contextPct ||
|
|
750
|
+
prev.permission_mode !== permissionMode
|
|
732
751
|
) {
|
|
733
752
|
lastByName.set(s.session_name, {
|
|
734
753
|
status,
|
|
735
754
|
context_pct: contextPct,
|
|
755
|
+
permission_mode: permissionMode,
|
|
736
756
|
})
|
|
737
757
|
client.send({
|
|
738
758
|
type: "session.state",
|
|
739
759
|
session_name: s.session_name,
|
|
740
760
|
status,
|
|
741
761
|
context_pct: contextPct,
|
|
762
|
+
permission_mode: permissionMode,
|
|
742
763
|
})
|
|
743
764
|
}
|
|
744
765
|
}
|
|
@@ -1046,7 +1067,7 @@ async function dispatch(msg, ctx) {
|
|
|
1046
1067
|
if (!ctx.claudeBridge) return
|
|
1047
1068
|
ctx.claudeBridge.interrupt({ stream_id: msg.stream_id })
|
|
1048
1069
|
return
|
|
1049
|
-
case "claude.tui.viewing":
|
|
1070
|
+
case "claude.tui.viewing": {
|
|
1050
1071
|
// T04778 watcher ゲート: browser が TUI チャットでこの session を閲覧中。
|
|
1051
1072
|
// 閲覧マーカーを更新し、承認フックが「閲覧者あり」と判定できるようにする。
|
|
1052
1073
|
ctx.tuiViewerRegistry
|
|
@@ -1056,7 +1077,59 @@ async function dispatch(msg, ctx) {
|
|
|
1056
1077
|
ctx.jsonlLiveWatchers
|
|
1057
1078
|
?.note({ session_id: msg.session_id, cwd: msg.cwd })
|
|
1058
1079
|
.catch(() => {})
|
|
1080
|
+
// T04786 後続: session 回転検知 (/clear で裏 claude が新 session_id へ切替)。
|
|
1081
|
+
// 追従中ビュー (follow_newest 既定 true) のときだけ、cwd の最新 session_id と
|
|
1082
|
+
// 閲覧中 id を突き合わせ、ズレていれば claude.session.rotated を push してフロントに
|
|
1083
|
+
// 読み出し先の切替を促す。過去セッションを意図的に開いているビュー (follow_newest=false)
|
|
1084
|
+
// では検知しない (ピン留め閲覧を最新へ引きずらない)。
|
|
1085
|
+
const viewSid = msg.session_id
|
|
1086
|
+
const viewCwd = msg.cwd
|
|
1087
|
+
const viewName =
|
|
1088
|
+
typeof msg.session_name === "string" ? msg.session_name : ""
|
|
1089
|
+
const followNewest = msg.follow_newest !== false
|
|
1090
|
+
if (followNewest && viewSid && viewCwd) {
|
|
1091
|
+
;(async () => {
|
|
1092
|
+
try {
|
|
1093
|
+
const projectsRoot = await getActiveProjectsRoot()
|
|
1094
|
+
const { sessions } = await listSessions({
|
|
1095
|
+
cwd: viewCwd,
|
|
1096
|
+
projectsRoot,
|
|
1097
|
+
limit: 1,
|
|
1098
|
+
logger,
|
|
1099
|
+
})
|
|
1100
|
+
const newestId = sessions?.[0]?.session_id || null
|
|
1101
|
+
const key = viewName || viewCwd
|
|
1102
|
+
const { rotated, newSessionId } = decideSessionRotation({
|
|
1103
|
+
viewingSessionId: viewSid,
|
|
1104
|
+
newestSessionId: newestId,
|
|
1105
|
+
lastNotifiedNewId: ctx.tuiRotationNotified.get(key),
|
|
1106
|
+
})
|
|
1107
|
+
if (!rotated) return
|
|
1108
|
+
ctx.tuiRotationNotified.set(key, newSessionId)
|
|
1109
|
+
// 冪等マップも新 id へ更新し、将来の remount bind が旧 id へ
|
|
1110
|
+
// respawn (claude 再起動) してしまうのを防ぐ。
|
|
1111
|
+
if (viewName) ctx.tuiReboundSessions.set(viewName, newSessionId)
|
|
1112
|
+
ctx.client.send({
|
|
1113
|
+
type: "claude.session.rotated",
|
|
1114
|
+
cwd: viewCwd,
|
|
1115
|
+
session_name: viewName || undefined,
|
|
1116
|
+
old_session_id: viewSid,
|
|
1117
|
+
new_session_id: newSessionId,
|
|
1118
|
+
})
|
|
1119
|
+
logger.info(
|
|
1120
|
+
{ session: viewName, cwd: viewCwd, old: viewSid, new: newSessionId },
|
|
1121
|
+
"tui session rotated (/clear) → notified browser",
|
|
1122
|
+
)
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
logger.warn(
|
|
1125
|
+
{ err: err?.message, cwd: viewCwd },
|
|
1126
|
+
"tui session rotation check failed",
|
|
1127
|
+
)
|
|
1128
|
+
}
|
|
1129
|
+
})()
|
|
1130
|
+
}
|
|
1059
1131
|
return
|
|
1132
|
+
}
|
|
1060
1133
|
case "claude.tui.unviewing":
|
|
1061
1134
|
// 閲覧終了。session_id マーカーを即失効させ、以降の Bash を即 ask に倒す。
|
|
1062
1135
|
ctx.tuiViewerRegistry
|
|
@@ -1065,6 +1138,74 @@ async function dispatch(msg, ctx) {
|
|
|
1065
1138
|
// jsonl tail も即停止 (閲覧していない session を追従し続けない)。
|
|
1066
1139
|
ctx.jsonlLiveWatchers?.unwatch({ session_id: msg.session_id })
|
|
1067
1140
|
return
|
|
1141
|
+
case "claude.tui.cyclePermission": {
|
|
1142
|
+
// 権限バッジ押下 → 対話 claude TUI へ shift+tab を送って権限モードを循環。
|
|
1143
|
+
// 旧実装は frontend が raw pty.data (BACKTAB) を送り、各端末が楽観値を
|
|
1144
|
+
// 個別に 1 段進めるだけだったため、複数端末間でズレ、かつ tmux 上で直接
|
|
1145
|
+
// 切り替えた変更とも乖離した。ここで agent が実キー送出 → ペイン再読込で
|
|
1146
|
+
// 実モードを確定 → 全ブラウザへ claude.tui.permission を broadcast し、
|
|
1147
|
+
// 「実際に動いているターミナルの状態」を正本として全端末に同期する。
|
|
1148
|
+
const cwd = typeof msg.cwd === "string" ? msg.cwd : ""
|
|
1149
|
+
const sessionName =
|
|
1150
|
+
typeof msg.session_name === "string" ? msg.session_name : ""
|
|
1151
|
+
if (!sessionName) return
|
|
1152
|
+
;(async () => {
|
|
1153
|
+
try {
|
|
1154
|
+
await cyclePermissionMode(sessionName, { logger })
|
|
1155
|
+
// TUI がフッターバナーを再描画するまで少し待ってから実モードを読む。
|
|
1156
|
+
await new Promise((r) => setTimeout(r, 250))
|
|
1157
|
+
const text = await capturePane(sessionName, { noCache: true })
|
|
1158
|
+
const mode = detectPermissionModeFromText(text)
|
|
1159
|
+
ctx.client.send({
|
|
1160
|
+
type: "claude.tui.permission",
|
|
1161
|
+
cwd: cwd || undefined,
|
|
1162
|
+
session_name: sessionName,
|
|
1163
|
+
permission_mode: mode,
|
|
1164
|
+
})
|
|
1165
|
+
logger.info(
|
|
1166
|
+
{ session: sessionName, cwd, mode },
|
|
1167
|
+
"tui permission cycled → notified browser",
|
|
1168
|
+
)
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
logger.warn(
|
|
1171
|
+
{ err: err?.message, session: sessionName },
|
|
1172
|
+
"claude.tui.cyclePermission failed",
|
|
1173
|
+
)
|
|
1174
|
+
}
|
|
1175
|
+
})()
|
|
1176
|
+
return
|
|
1177
|
+
}
|
|
1178
|
+
case "claude.tui.probePermission": {
|
|
1179
|
+
// 読み取り専用の権限モード問い合わせ (cold-load seed)。キーは送らず、ペインを
|
|
1180
|
+
// 読んで現在の実モードを claude.tui.permission として broadcast するだけ。
|
|
1181
|
+
// ブラウザがマウント直後に「今ターミナルが何モードか」を取得し、楽観値や
|
|
1182
|
+
// 古い jsonl 値とのズレを開始時点から揃えるために使う。検出不能 (claude 起動中・
|
|
1183
|
+
// 素のシェル) のときは mode=null となり、frontend 側は据え置く。
|
|
1184
|
+
const cwd = typeof msg.cwd === "string" ? msg.cwd : ""
|
|
1185
|
+
const sessionName =
|
|
1186
|
+
typeof msg.session_name === "string" ? msg.session_name : ""
|
|
1187
|
+
if (!sessionName) return
|
|
1188
|
+
;(async () => {
|
|
1189
|
+
try {
|
|
1190
|
+
const text = await capturePane(sessionName, { noCache: true })
|
|
1191
|
+
const mode = detectPermissionModeFromText(text)
|
|
1192
|
+
// 検出できた時だけ通知する (null で badge を消さない)。
|
|
1193
|
+
if (!mode) return
|
|
1194
|
+
ctx.client.send({
|
|
1195
|
+
type: "claude.tui.permission",
|
|
1196
|
+
cwd: cwd || undefined,
|
|
1197
|
+
session_name: sessionName,
|
|
1198
|
+
permission_mode: mode,
|
|
1199
|
+
})
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
logger.warn(
|
|
1202
|
+
{ err: err?.message, session: sessionName },
|
|
1203
|
+
"claude.tui.probePermission failed",
|
|
1204
|
+
)
|
|
1205
|
+
}
|
|
1206
|
+
})()
|
|
1207
|
+
return
|
|
1208
|
+
}
|
|
1068
1209
|
case "claude.tui.bind": {
|
|
1069
1210
|
// T04784 会話継続バインディング: TUI モード突入時に 1 回送られる。
|
|
1070
1211
|
// 1. 対象 cwd の最新 session_id を確定 (browser 指定があれば優先)。
|
package/src/state.mjs
CHANGED
|
@@ -82,6 +82,43 @@ export function detectContextPctFromText(text) {
|
|
|
82
82
|
return null
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// 対話 claude TUI のフッターに出る権限モードバナー (claude v2.1 系で実機確認)。
|
|
86
|
+
// shift+tab で循環: default(バナー無し) → accept edits → plan → auto → …
|
|
87
|
+
const PERMISSION_BANNERS = [
|
|
88
|
+
[/⏵⏵\s*accept edits on/i, "acceptEdits"],
|
|
89
|
+
[/⏸\s*plan mode on/i, "plan"],
|
|
90
|
+
[/⏵⏵\s*auto mode on/i, "auto"],
|
|
91
|
+
[/bypass permissions on/i, "bypassPermissions"],
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 対話 claude TUI のペインテキストから現在の権限モードを判定する。
|
|
96
|
+
*
|
|
97
|
+
* 全端末で「実際に動いているターミナルの権限状態」を同期するための正本。
|
|
98
|
+
* jsonl の permissionMode は送信ターン毎にしか記録されないため、shift+tab した
|
|
99
|
+
* だけ (未送信) の変更や tmux 上で直接切り替えた変更は jsonl に出ない。ペインの
|
|
100
|
+
* フッターバナーは shift+tab 直後に即更新されるので、これを正本にする。
|
|
101
|
+
*
|
|
102
|
+
* - 明示バナー (accept edits / plan / auto / bypass) があればそのモード。
|
|
103
|
+
* - バナーが無い場合、claude TUI のフッターが見えている時だけ "default" と判定する
|
|
104
|
+
* (素のシェルを default と誤検出しないためのガード)。
|
|
105
|
+
* - claude TUI と判別できなければ null (= 不明、呼び出し側は据え置き)。
|
|
106
|
+
*
|
|
107
|
+
* @param {string} text capturePane の結果 (ANSI 除去済み)
|
|
108
|
+
* @returns {string|null}
|
|
109
|
+
*/
|
|
110
|
+
export function detectPermissionModeFromText(text) {
|
|
111
|
+
if (!text) return null
|
|
112
|
+
for (const [re, mode] of PERMISSION_BANNERS) {
|
|
113
|
+
if (re.test(text)) return mode
|
|
114
|
+
}
|
|
115
|
+
// バナー無し = default。ただし claude TUI のフッターが見えている時だけ assert する。
|
|
116
|
+
if (/← for agents|for shortcuts|shift\+tab to cycle/i.test(text)) {
|
|
117
|
+
return "default"
|
|
118
|
+
}
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
85
122
|
export async function listSessionNames(opts = {}) {
|
|
86
123
|
const tmuxBin = opts.tmuxBin || "tmux"
|
|
87
124
|
try {
|
|
@@ -165,6 +202,7 @@ export async function detectSessionState(sessionName, opts = {}) {
|
|
|
165
202
|
const text = await capturePane(sessionName, opts)
|
|
166
203
|
const defaultStatus = detectStatusFromText(text)
|
|
167
204
|
const defaultContextPct = detectContextPctFromText(text)
|
|
205
|
+
const defaultPermissionMode = detectPermissionModeFromText(text)
|
|
168
206
|
|
|
169
207
|
if (opts.plugins && opts.plugins.length) {
|
|
170
208
|
const hookResult = await runHookChain(opts.plugins, "transformStatusDetection", {
|
|
@@ -177,11 +215,17 @@ export async function detectSessionState(sessionName, opts = {}) {
|
|
|
177
215
|
return {
|
|
178
216
|
status: hookResult.result.status || defaultStatus,
|
|
179
217
|
context_pct: hookResult.result.context_pct ?? defaultContextPct,
|
|
218
|
+
permission_mode:
|
|
219
|
+
hookResult.result.permission_mode ?? defaultPermissionMode,
|
|
180
220
|
}
|
|
181
221
|
}
|
|
182
222
|
}
|
|
183
223
|
|
|
184
|
-
return {
|
|
224
|
+
return {
|
|
225
|
+
status: defaultStatus,
|
|
226
|
+
context_pct: defaultContextPct,
|
|
227
|
+
permission_mode: defaultPermissionMode,
|
|
228
|
+
}
|
|
185
229
|
}
|
|
186
230
|
|
|
187
231
|
/** 全 session の現在状態を取得する。cwd も含める (chat 信号照合用)。 */
|
package/src/tmux.mjs
CHANGED
|
@@ -716,6 +716,53 @@ export function buildResumeCmd(sessionId, opts = {}) {
|
|
|
716
716
|
|
|
717
717
|
const _delay = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
718
718
|
|
|
719
|
+
/**
|
|
720
|
+
* 対話 claude TUI に shift+tab (BACKTAB) を 1 回送って権限モードを循環させる。
|
|
721
|
+
*
|
|
722
|
+
* 全端末で「実際に動いているターミナルの権限状態」を同期する仕組みの書込側
|
|
723
|
+
* (読込側は state.mjs の detectPermissionModeFromText)。フロントからの権限バッジ
|
|
724
|
+
* 押下は raw pty.data ではなく `claude.tui.cyclePermission` を送り、agent 側で
|
|
725
|
+
* 本関数を実行 → ペイン再読込で実モードを確定 → 全ブラウザへ broadcast する。
|
|
726
|
+
* これにより楽観値が端末間でズレず、jsonl 未記録 (未送信) の変更も即同期される。
|
|
727
|
+
*
|
|
728
|
+
* copy-mode 等に入っているとキーが奪われるので、入っていれば先に抜ける
|
|
729
|
+
* (フロントの cancelTmuxMode 相当を agent 側でも担保)。ベストエフォート。
|
|
730
|
+
*
|
|
731
|
+
* @param {string} name tmux セッション名
|
|
732
|
+
* @param {{logger?:object,tmuxBin?:string}} [opts]
|
|
733
|
+
* @returns {Promise<{ok:boolean, error?:string}>}
|
|
734
|
+
*/
|
|
735
|
+
export async function cyclePermissionMode(name, opts = {}) {
|
|
736
|
+
const bin = tmuxBin(opts)
|
|
737
|
+
try {
|
|
738
|
+
// copy-mode 等に入っていると BTab が奪われるので、入っている時だけ抜ける。
|
|
739
|
+
try {
|
|
740
|
+
const { stdout } = await execFileP(bin, [
|
|
741
|
+
"display-message",
|
|
742
|
+
"-p",
|
|
743
|
+
"-t",
|
|
744
|
+
`${name}:`,
|
|
745
|
+
"-F",
|
|
746
|
+
"#{pane_in_mode}",
|
|
747
|
+
])
|
|
748
|
+
if (stdout.trim() === "1") {
|
|
749
|
+
await execFileP(bin, ["send-keys", "-t", name, "-X", "cancel"])
|
|
750
|
+
}
|
|
751
|
+
} catch {
|
|
752
|
+
// pane_in_mode 取得失敗はベストエフォートで無視 (そのまま BTab を送る)。
|
|
753
|
+
}
|
|
754
|
+
// BTab = shift+tab。claude TUI が権限モードを 1 段循環する。
|
|
755
|
+
await execFileP(bin, ["send-keys", "-t", name, "BTab"])
|
|
756
|
+
return { ok: true }
|
|
757
|
+
} catch (err) {
|
|
758
|
+
opts.logger?.warn(
|
|
759
|
+
{ session: name, err: err?.message },
|
|
760
|
+
"cyclePermissionMode failed",
|
|
761
|
+
)
|
|
762
|
+
return { ok: false, error: err?.message || String(err) }
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
719
766
|
/**
|
|
720
767
|
* tmux セッションの対話 claude を、指定 session_id に `--resume` で載せ替える
|
|
721
768
|
* (T04784 TUI resume binding)。
|