tailii-host 0.5.0 → 0.7.0
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/dist/approval/hook.js +11 -1
- package/dist/approval/hook.js.map +1 -1
- package/dist/backend/herdr.d.ts +60 -6
- package/dist/backend/herdr.js +180 -27
- package/dist/backend/herdr.js.map +1 -1
- package/dist/backend/sessionBackend.d.ts +22 -3
- package/dist/backend/sessionBackend.js +11 -2
- package/dist/backend/sessionBackend.js.map +1 -1
- package/dist/backend/tmux.d.ts +197 -3
- package/dist/backend/tmux.js +565 -16
- package/dist/backend/tmux.js.map +1 -1
- package/dist/chat/chatTailController.d.ts +4 -2
- package/dist/chat/chatTailController.js +19 -33
- package/dist/chat/chatTailController.js.map +1 -1
- package/dist/chat/imageService.d.ts +9 -1
- package/dist/chat/imageService.js +39 -0
- package/dist/chat/imageService.js.map +1 -1
- package/dist/chat/subagentTailer.d.ts +10 -0
- package/dist/chat/subagentTailer.js +234 -51
- package/dist/chat/subagentTailer.js.map +1 -1
- package/dist/chat/subagentTranscript.js +51 -5
- package/dist/chat/subagentTranscript.js.map +1 -1
- package/dist/chat/transcriptTailer.d.ts +53 -1
- package/dist/chat/transcriptTailer.js +313 -2
- package/dist/chat/transcriptTailer.js.map +1 -1
- package/dist/codex/codexAppServer.d.ts +54 -9
- package/dist/codex/codexAppServer.js +495 -76
- package/dist/codex/codexAppServer.js.map +1 -1
- package/dist/codex/codexNativeTurnController.d.ts +22 -1
- package/dist/codex/codexNativeTurnController.js +76 -11
- package/dist/codex/codexNativeTurnController.js.map +1 -1
- package/dist/codex/codexRolloutTailer.d.ts +3 -0
- package/dist/codex/codexRolloutTailer.js +85 -13
- package/dist/codex/codexRolloutTailer.js.map +1 -1
- package/dist/codex/codexSessionStore.js +54 -16
- package/dist/codex/codexSessionStore.js.map +1 -1
- package/dist/codex/codexSubagentTracker.d.ts +13 -0
- package/dist/codex/codexSubagentTracker.js +55 -1
- package/dist/codex/codexSubagentTracker.js.map +1 -1
- package/dist/codex/codexSystemNotice.d.ts +18 -0
- package/dist/codex/codexSystemNotice.js +212 -0
- package/dist/codex/codexSystemNotice.js.map +1 -0
- package/dist/commands/launch.js +50 -13
- package/dist/commands/launch.js.map +1 -1
- package/dist/engine/context.d.ts +32 -1
- package/dist/engine/context.js +39 -3
- package/dist/engine/context.js.map +1 -1
- package/dist/engine/engine.d.ts +5 -1
- package/dist/engine/engine.js +87 -4
- package/dist/engine/engine.js.map +1 -1
- package/dist/engine/handlers/accountUsage.d.ts +17 -0
- package/dist/engine/handlers/accountUsage.js +40 -6
- package/dist/engine/handlers/accountUsage.js.map +1 -1
- package/dist/engine/handlers/codex.js +37 -0
- package/dist/engine/handlers/codex.js.map +1 -1
- package/dist/engine/handlers/core.js +14 -1
- package/dist/engine/handlers/core.js.map +1 -1
- package/dist/engine/handlers/mode.js +105 -1
- package/dist/engine/handlers/mode.js.map +1 -1
- package/dist/engine/handlers/session.js +17 -33
- package/dist/engine/handlers/session.js.map +1 -1
- package/dist/engine/handlers/usage.js +4 -1
- package/dist/engine/handlers/usage.js.map +1 -1
- package/dist/engine/handlers/workspace.js +22 -1
- package/dist/engine/handlers/workspace.js.map +1 -1
- package/dist/hub/effortDialog.d.ts +15 -0
- package/dist/hub/effortDialog.js +60 -16
- package/dist/hub/effortDialog.js.map +1 -1
- package/dist/hub/engineRelaySocket.d.ts +7 -0
- package/dist/hub/engineRelaySocket.js +7 -1
- package/dist/hub/engineRelaySocket.js.map +1 -1
- package/dist/hub/hubDaemon.js +30 -5
- package/dist/hub/hubDaemon.js.map +1 -1
- package/dist/hub/hubProtocol.d.ts +10 -0
- package/dist/hub/hubProtocol.js +19 -2
- package/dist/hub/hubProtocol.js.map +1 -1
- package/dist/hub/panePreviewPump.d.ts +27 -0
- package/dist/hub/panePreviewPump.js +62 -1
- package/dist/hub/panePreviewPump.js.map +1 -1
- package/dist/hub/sessionHub.d.ts +125 -4
- package/dist/hub/sessionHub.js +413 -48
- package/dist/hub/sessionHub.js.map +1 -1
- package/dist/protocol/decode.js +84 -15
- package/dist/protocol/decode.js.map +1 -1
- package/dist/protocol/encode.js +2 -0
- package/dist/protocol/encode.js.map +1 -1
- package/dist/protocol/messages.d.ts +87 -1
- package/dist/services/accountIdentity.js +3 -1
- package/dist/services/accountIdentity.js.map +1 -1
- package/dist/services/fileService.d.ts +15 -0
- package/dist/services/fileService.js +176 -14
- package/dist/services/fileService.js.map +1 -1
- package/dist/services/officialApps.d.ts +21 -1
- package/dist/services/officialApps.js +201 -53
- package/dist/services/officialApps.js.map +1 -1
- package/dist/services/planUsageFetcher.d.ts +35 -3
- package/dist/services/planUsageFetcher.js +34 -6
- package/dist/services/planUsageFetcher.js.map +1 -1
- package/dist/services/previewServer.js +15 -6
- package/dist/services/previewServer.js.map +1 -1
- package/dist/sessions/claudeSessionStore.js +30 -0
- package/dist/sessions/claudeSessionStore.js.map +1 -1
- package/dist/sessions/heartbeat.d.ts +7 -1
- package/dist/sessions/heartbeat.js +3 -1
- package/dist/sessions/heartbeat.js.map +1 -1
- package/dist/sessions/sessionSearch.js +23 -5
- package/dist/sessions/sessionSearch.js.map +1 -1
- package/dist/sessions/transcriptPath.d.ts +3 -0
- package/dist/sessions/transcriptPath.js +15 -0
- package/dist/sessions/transcriptPath.js.map +1 -0
- package/dist/shared/clientBuild.d.ts +26 -0
- package/dist/shared/clientBuild.js +89 -0
- package/dist/shared/clientBuild.js.map +1 -0
- package/dist/shared/crossSession.d.ts +15 -0
- package/dist/shared/crossSession.js +75 -0
- package/dist/shared/crossSession.js.map +1 -0
- package/dist/shared/harnessReminder.d.ts +32 -0
- package/dist/shared/harnessReminder.js +124 -0
- package/dist/shared/harnessReminder.js.map +1 -0
- package/dist/shared/systemNotice.d.ts +30 -0
- package/dist/shared/systemNotice.js +209 -0
- package/dist/shared/systemNotice.js.map +1 -0
- package/package.json +3 -3
package/dist/backend/tmux.js
CHANGED
|
@@ -92,6 +92,20 @@ export function extractClaudeInputBox(screen) {
|
|
|
92
92
|
}
|
|
93
93
|
return null;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* claude TUI の入力欄(composer)が描画済みか(起動中の pane との区別, boot-gate)。
|
|
97
|
+
*
|
|
98
|
+
* claude プロセスは起動していても、TUI が入力欄を描くまでには(host 負荷次第で)数秒〜
|
|
99
|
+
* 10 秒超の窓がある。その間の画面は初期シェルのプロンプト/`exec zsh -lc …` のエコー/
|
|
100
|
+
* 起動バナー(`╭…╮` の角付き枠)だけで、入力欄の罫線(`─` の連続行)は 1 本も無い。
|
|
101
|
+
* 罫線が 1 本でも見えれば TUI は描画済み(composer は本文の罫線と同じフレームで描かれ、
|
|
102
|
+
* スクロールで上側の罫線が窓外に出ても下側は残る)。`❯` だけの画面はシェルプロンプトの
|
|
103
|
+
* 可能性があるため描画済みとみなさない(`extractClaudeInputBox` の ❯ フォールバックは
|
|
104
|
+
* 描画済み前提の判定にだけ使う)。
|
|
105
|
+
*/
|
|
106
|
+
export function claudeInputBoxRendered(screen) {
|
|
107
|
+
return screen.split("\n").some((line) => isInputBoxRuleLine(stripSgr(line).trim()));
|
|
108
|
+
}
|
|
95
109
|
/**
|
|
96
110
|
* 入力が空のとき claude TUI が入力欄へ薄字で出すプレースホルダ(実測 2.1.220)。
|
|
97
111
|
* 未送信テキストと誤認すると、シェルモード離脱(空入力の Backspace)に入れず
|
|
@@ -108,6 +122,418 @@ function isInputPlaceholder(text) {
|
|
|
108
122
|
return true;
|
|
109
123
|
return text.startsWith("Message @") && text.endsWith("…");
|
|
110
124
|
}
|
|
125
|
+
/** SGR カラー/属性エスケープ(`ESC[…m`)を除いた素のテキスト。 */
|
|
126
|
+
function stripSgr(line) {
|
|
127
|
+
// eslint-disable-next-line no-control-regex
|
|
128
|
+
return line.replace(/\u001b\[[0-9;]*m/g, "");
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* claude TUI の「プロンプト提案」(2.1.25x 頃〜。空の入力欄に薄字で次の一手を提示し、
|
|
132
|
+
* → / Tab で採用、Enter で即送信される)と空入力プレースホルダーは、いずれも通常の
|
|
133
|
+
* 未送信テキストと**同じ入力欄位置**に描画される。`--format text`(SGR 除去済み)では
|
|
134
|
+
* 本文と区別できないため、注入の残留 flush 判定・送信確定ループがこれらを実テキストと
|
|
135
|
+
* 誤認し、Enter で提案をそのまま送信してしまう(実障害 2026-09-03: iPhone 送信のたびに
|
|
136
|
+
* AI の提案文が勝手に送られ、送信が壊れる)。
|
|
137
|
+
*
|
|
138
|
+
* 決定的シグナル: 提案もプレースホルダーも本文が SGR 2(faint/薄字)で描画される。
|
|
139
|
+
* 利用者入力・中断時の queued 書き戻しは faint ではない(実測 herdr `pane read --format
|
|
140
|
+
* ansi` 2026-09-03: 提案は 本文が ESC[2m 包み、実テキストは faint 無し)。
|
|
141
|
+
*
|
|
142
|
+
* ANSI 画面から入力欄本文を切り出し、本文の可視文字がすべて faint なら「実テキスト無し」
|
|
143
|
+
* とみなす(=提案/プレースホルダー)。faint でない可視文字が 1 つでもあれば実テキスト有り。
|
|
144
|
+
* 判定不能(罫線が見つからない・本文空)は false。
|
|
145
|
+
*/
|
|
146
|
+
export function inputBoxHasRealPendingText(ansiScreen) {
|
|
147
|
+
const analysis = analyzeInputBox(ansiScreen);
|
|
148
|
+
return analysis !== null && analysis.text.length > 0 && !analysis.faintOnly;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 入力欄の「実テキスト」(薄字プロンプト提案・プレースホルダーを除いた未送信本文)を返す。
|
|
152
|
+
* `null` = 入力欄が見つからない(罫線が窓外・描画崩れ)、`""` = 空 or 提案/プレースホルダー、
|
|
153
|
+
* それ以外 = 実テキスト。text capture の box.text を使う判定(clearInputBox の空判定・
|
|
154
|
+
* inputBoxContainsText の probe 照合)が提案を実テキストと誤認するのを塞ぐため、ANSI から
|
|
155
|
+
* faint を見て実テキストだけを取り出す(prompt-suggestion-chip)。
|
|
156
|
+
*/
|
|
157
|
+
export function inputBoxRealText(ansiScreen) {
|
|
158
|
+
const analysis = analyzeInputBox(ansiScreen);
|
|
159
|
+
if (analysis === null)
|
|
160
|
+
return null;
|
|
161
|
+
return analysis.faintOnly ? "" : analysis.text;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* 入力欄の残存テキストが、transcript に記録済みの発話本文と同文か(restored-prompt-discard)。
|
|
165
|
+
*
|
|
166
|
+
* 入力欄の描画は折り返し(trim + 改行連結)・composer スクロールで先頭行が窓外に出る・シェルモード
|
|
167
|
+
* 記号 `!` の吸い上げがあるため、生の等値比較は使えない。空白類を両辺から除去し、先頭の `!` を
|
|
168
|
+
* 落としたうえで「同一」または「残存が記録本文の末尾(可視領域は常に末尾側)」なら同文とみなす。
|
|
169
|
+
* 末尾一致は短い断片の偶然一致(Mac 側で打ちかけた下書きが記録本文の語尾と重なる等)を避けるため
|
|
170
|
+
* 24 字以上に限る(typedTextProbe と同じ長さ)。
|
|
171
|
+
*/
|
|
172
|
+
export function inputBoxTextMatchesRecordedPrompt(boxText, recordedPrompt) {
|
|
173
|
+
const normalize = (value) => value.replace(/\s+/g, "").replace(/^!/, "");
|
|
174
|
+
const pending = normalize(boxText);
|
|
175
|
+
const recorded = normalize(recordedPrompt);
|
|
176
|
+
if (pending.length === 0 || recorded.length === 0)
|
|
177
|
+
return false;
|
|
178
|
+
if (pending === recorded)
|
|
179
|
+
return true;
|
|
180
|
+
return pending.length >= 24 && recorded.endsWith(pending);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* claude TUI のプロンプト提案文を取り出す(提案チップ表示用)。
|
|
184
|
+
* 入力欄本文が薄字(faint)のみ かつ 既知プレースホルダーでない非空テキストのときだけ、その
|
|
185
|
+
* 本文を返す(=提案)。空・実テキスト(利用者入力/中断書き戻し)・プレースホルダーは null。
|
|
186
|
+
* inputBoxHasRealPendingText と同じ解析(analyzeInputBox)を共有し、判定を一貫させる。
|
|
187
|
+
*/
|
|
188
|
+
export function extractInputBoxSuggestion(ansiScreen) {
|
|
189
|
+
const analysis = analyzeInputBox(ansiScreen);
|
|
190
|
+
if (analysis === null || analysis.text.length === 0)
|
|
191
|
+
return null;
|
|
192
|
+
return analysis.faintOnly ? analysis.text : null;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* ANSI 画面から入力欄本文の可視テキスト(既知プレースホルダーは "" へ、extractClaudeInputBox
|
|
196
|
+
* 準拠)と「本文がすべて薄字(faint)か」を返す。罫線ペア or 末尾 `❯` 行で領域を特定する
|
|
197
|
+
* (extractClaudeInputBox と同型)。入力欄が見つからなければ null。
|
|
198
|
+
*/
|
|
199
|
+
function analyzeInputBox(ansiScreen) {
|
|
200
|
+
const rawLines = ansiScreen.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
201
|
+
const stripped = rawLines.map((line) => stripSgr(line).trim());
|
|
202
|
+
// 罫線ペアで入力欄領域を特定する(extractClaudeInputBox と同じ規則を SGR 除去後の行へ)。
|
|
203
|
+
let bottom = -1;
|
|
204
|
+
for (let index = stripped.length - 1; index >= 0; index -= 1) {
|
|
205
|
+
if (isInputBoxRuleLine(stripped[index] ?? "")) {
|
|
206
|
+
bottom = index;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
let top = -1;
|
|
211
|
+
for (let index = bottom - 1; index >= 0; index -= 1) {
|
|
212
|
+
if (isInputBoxRuleLine(stripped[index] ?? "")) {
|
|
213
|
+
top = index;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
let bodyRaw;
|
|
218
|
+
if (top >= 0 && bottom > top) {
|
|
219
|
+
bodyRaw = rawLines.slice(top + 1, bottom);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// 罫線が無い画面は最後の `❯` 行 1 行だけを入力欄とみなす(extractClaudeInputBox と同型)。
|
|
223
|
+
let sigilIndex = -1;
|
|
224
|
+
for (let index = stripped.length - 1; index >= 0; index -= 1) {
|
|
225
|
+
if ((stripped[index] ?? "").startsWith("❯")) {
|
|
226
|
+
sigilIndex = index;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (sigilIndex < 0)
|
|
231
|
+
return null;
|
|
232
|
+
bodyRaw = [rawLines[sigilIndex] ?? ""];
|
|
233
|
+
}
|
|
234
|
+
// プレースホルダー(文言一致)は "" 扱い(extractClaudeInputBox が担保・後方互換)。
|
|
235
|
+
const box = extractClaudeInputBox(stripped.join("\n"));
|
|
236
|
+
if (box === null)
|
|
237
|
+
return null;
|
|
238
|
+
return { text: box.text, faintOnly: bodyIsFaintOnly(bodyRaw) };
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* 入力欄本文(ANSI 付き raw 行)の可視文字がすべて faint(SGR 2)か。
|
|
242
|
+
* SGR 状態を文字送りで追い(0 = 全リセット / 2 = faint on / 22 = faint off)、
|
|
243
|
+
* 空白・モード記号(`❯ › !`)以外の可視文字だけを評価する。可視文字が 1 つも無ければ false。
|
|
244
|
+
*/
|
|
245
|
+
function bodyIsFaintOnly(bodyRaw) {
|
|
246
|
+
const ESC = "\u001b";
|
|
247
|
+
const NBSP = " ";
|
|
248
|
+
let faint = false;
|
|
249
|
+
let sawPrintable = false;
|
|
250
|
+
let allFaint = true;
|
|
251
|
+
for (const line of bodyRaw) {
|
|
252
|
+
let index = 0;
|
|
253
|
+
while (index < line.length) {
|
|
254
|
+
if (line[index] === ESC && line[index + 1] === "[") {
|
|
255
|
+
// eslint-disable-next-line no-control-regex
|
|
256
|
+
const match = /^\u001b\[([0-9;]*)m/.exec(line.slice(index));
|
|
257
|
+
if (match) {
|
|
258
|
+
const params = match[1] ?? "";
|
|
259
|
+
const codes = params === "" ? [0] : params.split(";").map((code) => Number(code));
|
|
260
|
+
// SGR を左から評価する。38/48(前景/背景色)は 2;r;g;b または 5;n の
|
|
261
|
+
// サブパラメータを従えるため、その分を読み飛ばす(`38;2;255;255;255` の `2` を
|
|
262
|
+
// faint(SGR 2) と誤認しない — この取り違えが色付き実テキストの誤判定原因だった)。
|
|
263
|
+
for (let cursor = 0; cursor < codes.length; cursor += 1) {
|
|
264
|
+
const code = codes[cursor];
|
|
265
|
+
if (code === 38 || code === 48) {
|
|
266
|
+
const mode = codes[cursor + 1];
|
|
267
|
+
cursor += mode === 2 ? 4 : mode === 5 ? 2 : 1;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (code === 0 || code === 22)
|
|
271
|
+
faint = false;
|
|
272
|
+
else if (code === 2)
|
|
273
|
+
faint = true;
|
|
274
|
+
}
|
|
275
|
+
index += match[0].length;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const ch = line[index] ?? "";
|
|
280
|
+
// 空白・NBSP・モード記号は本文の可視性判定から除外する(記号の色は faint とは限らない)。
|
|
281
|
+
if (ch !== " " &&
|
|
282
|
+
ch !== NBSP &&
|
|
283
|
+
ch !== "\t" &&
|
|
284
|
+
ch !== "❯" &&
|
|
285
|
+
ch !== "›" &&
|
|
286
|
+
ch !== "!") {
|
|
287
|
+
sawPrintable = true;
|
|
288
|
+
if (!faint)
|
|
289
|
+
allFaint = false;
|
|
290
|
+
}
|
|
291
|
+
index += 1;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return sawPrintable && allFaint;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* `/login` の OAuth コード入力待ち行(実測 claude 2.1.241: `Paste code here if prompted >`)。
|
|
298
|
+
* ブラウザで取得したコードを貼る唯一の入力面で、通常の入力欄(罫線ペア)は描画されない。
|
|
299
|
+
*/
|
|
300
|
+
export const LOGIN_CODE_PROMPT_MARKER = "Paste code here if prompted";
|
|
301
|
+
/**
|
|
302
|
+
* `/login` でコード送信が失敗した後の再試行待ち行(実測 2.1.241:
|
|
303
|
+
* `OAuth error: Request failed with status code 400` の下に `Press Enter to retry.`)。
|
|
304
|
+
*/
|
|
305
|
+
export const LOGIN_RETRY_MARKER = "Press Enter to retry";
|
|
306
|
+
/** `/login` 方式選択(`Select login method:`)のタイトル行(実測 2.1.241。フッターは `Esc to cancel` のみ)。 */
|
|
307
|
+
export const LOGIN_METHOD_MARKER = "Select login method";
|
|
308
|
+
/**
|
|
309
|
+
* 成功後の継続待ち画面(CLI 2.1.241 の文字列 `Login successful. Press Enter to continue…`。
|
|
310
|
+
* 入力欄は無く、Enter / Esc で閉じて通常の入力欄へ戻る)。
|
|
311
|
+
* 2026-08-25 実障害: この画面を知らず「交換中」として 10s 待って失敗を返し、利用者がキャンセル
|
|
312
|
+
* (Esc)すると閉じてログイン済みになる、という逆転が起きた。
|
|
313
|
+
*/
|
|
314
|
+
export const LOGIN_CONTINUE_MARKER = "to continue";
|
|
315
|
+
/** ダイアログ行が pane 末尾(最後の非空行)からこの行数以内にあるときだけ「生きている」とみなす。 */
|
|
316
|
+
const LOGIN_TAIL_WINDOW = 12;
|
|
317
|
+
/**
|
|
318
|
+
* `marker` で始まる行が、生きたダイアログとして画面にあるか(TESTABLE 内部)。
|
|
319
|
+
* 会話本文が同じ文言を行頭に含むだけ(Ink の折り返し・/login の説明文)で発火しないよう、
|
|
320
|
+
* (a) 末尾 LOGIN_TAIL_WINDOW 行以内 (b) 直下 `footerReach` 行以内に `Esc to cancel` フッター、
|
|
321
|
+
* の 2 条件を課す(iOS 側 ClaudeLoginPromptParser.isLiveDialogLine と同じ規則)。
|
|
322
|
+
*/
|
|
323
|
+
function liveDialogLineIndex(lines, marker, footerReach) {
|
|
324
|
+
let lastContent = -1;
|
|
325
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
326
|
+
if ((lines[index] ?? "").trim().length > 0) {
|
|
327
|
+
lastContent = index;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (lastContent < 0)
|
|
332
|
+
return null;
|
|
333
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
334
|
+
if (!(lines[index] ?? "").trim().startsWith(marker))
|
|
335
|
+
continue;
|
|
336
|
+
if (lastContent - index > LOGIN_TAIL_WINDOW)
|
|
337
|
+
return null;
|
|
338
|
+
const end = Math.min(lines.length, index + 1 + footerReach);
|
|
339
|
+
for (let below = index + 1; below < end; below += 1) {
|
|
340
|
+
if ((lines[below] ?? "").trim().toLowerCase().startsWith("esc to cancel"))
|
|
341
|
+
return index;
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
/** 画面が `/login` のコード入力待ちか(TESTABLE)。chat 注入の門番と login_code_send の前提確認に使う。 */
|
|
348
|
+
export function screenHasLoginCodePrompt(screen) {
|
|
349
|
+
return liveDialogLineIndex(screen.split("\n"), LOGIN_CODE_PROMPT_MARKER, 3) !== null;
|
|
350
|
+
}
|
|
351
|
+
function screenHasLoginRetry(screen) {
|
|
352
|
+
return liveDialogLineIndex(screen.split("\n"), LOGIN_RETRY_MARKER, 3) !== null;
|
|
353
|
+
}
|
|
354
|
+
function screenHasLoginMethodSelect(screen) {
|
|
355
|
+
// タイトル行と Esc フッターの間に選択肢(最大 3 行 + 空行)が入る。
|
|
356
|
+
return liveDialogLineIndex(screen.split("\n"), LOGIN_METHOD_MARKER, 8) !== null;
|
|
357
|
+
}
|
|
358
|
+
/** 成功後の継続待ち(`Login successful. Press Enter to continue…`)が pane 末尾付近にあるか。 */
|
|
359
|
+
export function screenHasLoginContinue(screen) {
|
|
360
|
+
const lines = screen.split("\n");
|
|
361
|
+
let lastContent = -1;
|
|
362
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
363
|
+
if ((lines[index] ?? "").trim().length > 0) {
|
|
364
|
+
lastContent = index;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (lastContent < 0)
|
|
369
|
+
return false;
|
|
370
|
+
for (let index = lastContent; index >= Math.max(0, lastContent - LOGIN_TAIL_WINDOW); index -= 1) {
|
|
371
|
+
const text = (lines[index] ?? "").trim().toLowerCase();
|
|
372
|
+
if (text.includes("login successful") && text.includes(LOGIN_CONTINUE_MARKER))
|
|
373
|
+
return true;
|
|
374
|
+
if (text.startsWith("press enter to continue"))
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* 画面が `/login` フローのどこか(方式選択 / コード入力待ち / 失敗後の再試行待ち)にいるか(TESTABLE)。
|
|
381
|
+
* chat 注入の門番用。コード欄以外の 2 画面も、通常本文 + Enter が選択リストや retry を
|
|
382
|
+
* 押してしまうため同じく拒否する。
|
|
383
|
+
*/
|
|
384
|
+
export function screenInLoginFlow(screen) {
|
|
385
|
+
return screenHasLoginCodePrompt(screen) || screenHasLoginRetry(screen)
|
|
386
|
+
|| screenHasLoginMethodSelect(screen) || screenHasLoginContinue(screen);
|
|
387
|
+
}
|
|
388
|
+
export function loginCodeScreenState(screen) {
|
|
389
|
+
if (screenHasLoginCodePrompt(screen))
|
|
390
|
+
return "prompt";
|
|
391
|
+
if (screenHasLoginRetry(screen))
|
|
392
|
+
return "retry";
|
|
393
|
+
if (screenHasLoginMethodSelect(screen))
|
|
394
|
+
return "method";
|
|
395
|
+
if (screenHasLoginContinue(screen))
|
|
396
|
+
return "continue";
|
|
397
|
+
const lines = screen.split("\n");
|
|
398
|
+
const tail = lines.slice(-LOGIN_TAIL_WINDOW - 8);
|
|
399
|
+
const rules = tail.filter((line) => isInputBoxRuleLine(line.trim())).length;
|
|
400
|
+
if (rules >= 2)
|
|
401
|
+
return "accepted";
|
|
402
|
+
return "pending";
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* retry 画面の理由行。`OAuth error` で始まる行(末尾窓内の最後のもの)だけを採用する。
|
|
406
|
+
* 直上行フォールバックは持たない(コードをエコーしたコード欄の行が理由として result / ログへ
|
|
407
|
+
* 漏れる)。念のためコード欄マーカーを含む行は採用しない。
|
|
408
|
+
*/
|
|
409
|
+
export function loginCodeErrorLine(screen) {
|
|
410
|
+
const lines = screen.split("\n").map((line) => line.trim());
|
|
411
|
+
const tail = lines.slice(-LOGIN_TAIL_WINDOW - 8);
|
|
412
|
+
for (let index = tail.length - 1; index >= 0; index -= 1) {
|
|
413
|
+
const text = tail[index] ?? "";
|
|
414
|
+
if (text.startsWith("OAuth error") && !text.includes(LOGIN_CODE_PROMPT_MARKER)) {
|
|
415
|
+
return text.slice(0, 200);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* login-code 経路の失敗(利用者へそのまま出せる文言だけを message に持つ)。
|
|
422
|
+
* コード本文を含む生の backend エラー(tmux の args 等)を result / diag に流さないための型。
|
|
423
|
+
* chat 注入の門番(1 キーも送る前の確定拒否)にも使う — hub はこの型を「未送出の失敗」として
|
|
424
|
+
* uncertain(配送不明)に積まない。
|
|
425
|
+
*/
|
|
426
|
+
export class LoginCodeError extends Error {
|
|
427
|
+
constructor(message) {
|
|
428
|
+
super(message);
|
|
429
|
+
this.name = "LoginCodeError";
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/** login_code_send が拒否するメッセージ(コード入力待ちでない)。 */
|
|
433
|
+
export const LOGIN_CODE_PROMPT_NOT_VISIBLE = "Claude はログインコードの入力待ちではありません(/login を実行してから送ってください)";
|
|
434
|
+
/** retry 画面で login_code_send されたときの案内。 */
|
|
435
|
+
export const LOGIN_CODE_RETRY_PENDING = "Claude は再試行待ちです。「もう一度」を押してから、新しいコードを送ってください";
|
|
436
|
+
/** chat 注入が `/login` フロー中の画面を検出したときの拒否文言(hub が会話本文へそのまま出す)。 */
|
|
437
|
+
export const CHAT_BLOCKED_BY_LOGIN_PROMPT = "Claude が /login の途中です。転写カードから操作するか、キャンセルしてから送ってください";
|
|
438
|
+
/** コード送出は届いたが、待ってもコード欄が消えない(CR 取りこぼし・TUI 停止)。 */
|
|
439
|
+
export const LOGIN_CODE_NOT_ACCEPTED = "コードが Claude に受理されませんでした(もう一度送るか、Mac 側の画面を確認してください)";
|
|
440
|
+
/** コード本文がコード欄に反映されなかった(send-text 取りこぼし)。CR は撃たない。 */
|
|
441
|
+
export const LOGIN_CODE_NOT_ECHOED = "コードが入力欄に反映されませんでした(もう一度送ってください)";
|
|
442
|
+
/** 交換が長引き、受理も拒否も確定しなかった。 */
|
|
443
|
+
export const LOGIN_CODE_UNSETTLED = "ログインの結果を確認できませんでした(Mac 側の画面を確認してください)";
|
|
444
|
+
/** login_code_send の失敗を利用者向け文言へ写す(コード本文・内部 args を漏らさない)。 */
|
|
445
|
+
export function loginCodeErrorMessage(error) {
|
|
446
|
+
if (error instanceof LoginCodeError)
|
|
447
|
+
return error.message;
|
|
448
|
+
const name = error instanceof Error ? error.name : "Error";
|
|
449
|
+
return `コードの送出に失敗しました(${name})`;
|
|
450
|
+
}
|
|
451
|
+
/** コード欄に本文が反映されたか(入力は末尾 6 文字以外マスク表示される実測に合わせ、末尾で照合)。 */
|
|
452
|
+
export function loginCodeEchoed(screen, code) {
|
|
453
|
+
const lines = screen.split("\n");
|
|
454
|
+
const index = liveDialogLineIndex(lines, LOGIN_CODE_PROMPT_MARKER, 3);
|
|
455
|
+
if (index === null)
|
|
456
|
+
return false;
|
|
457
|
+
const probe = code.slice(-Math.min(6, code.length));
|
|
458
|
+
return (lines[index] ?? "").includes(probe);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* `/login` のコードを送出し、結果を画面で確定させる(tmux / herdr 共通, TESTABLE)。
|
|
462
|
+
* - 送出前にコード欄が無ければ何も送らず throw(retry 画面は専用の案内)
|
|
463
|
+
* - 本文送出後、コード欄へのエコーを確認してから CR(反映していなければ CR を撃たず throw —
|
|
464
|
+
* 切り詰めたコードを送ってワンタイムコードを焼かない)
|
|
465
|
+
* - CR 後は `loginCodeScreenState` で待つ。retry / method は即 throw(余分な CR で retry を
|
|
466
|
+
* 押さない)。accepted(陽性証拠)で受理。prompt が settleMs 残れば **再キャプチャして prompt
|
|
467
|
+
* のときだけ** CR を 1 回再送。pending は settleMs×2 まで待ち、確定しなければ throw。
|
|
468
|
+
* 暗黙の ok は返さない。
|
|
469
|
+
*/
|
|
470
|
+
export async function submitLoginCode(code, ops) {
|
|
471
|
+
const now = ops.now ?? (() => Date.now());
|
|
472
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
473
|
+
const initial = await ops.capture();
|
|
474
|
+
const initialState = initial === null ? "pending" : loginCodeScreenState(initial);
|
|
475
|
+
if (initialState === "retry")
|
|
476
|
+
throw new LoginCodeError(LOGIN_CODE_RETRY_PENDING);
|
|
477
|
+
if (initialState !== "prompt")
|
|
478
|
+
throw new LoginCodeError(LOGIN_CODE_PROMPT_NOT_VISIBLE);
|
|
479
|
+
await ops.sendLiteral(code);
|
|
480
|
+
await sleep(ops.delayMs);
|
|
481
|
+
let echoed = false;
|
|
482
|
+
for (let attempt = 0; attempt < 2 && !echoed; attempt += 1) {
|
|
483
|
+
if (attempt > 0)
|
|
484
|
+
await sleep(ops.pollMs);
|
|
485
|
+
const screen = await ops.capture();
|
|
486
|
+
echoed = screen !== null && loginCodeEchoed(screen, code);
|
|
487
|
+
}
|
|
488
|
+
if (!echoed)
|
|
489
|
+
throw new LoginCodeError(LOGIN_CODE_NOT_ECHOED);
|
|
490
|
+
const start = now();
|
|
491
|
+
const pendingDeadline = start + ops.settleMs * 2;
|
|
492
|
+
let entersSent = 0;
|
|
493
|
+
let continueEnters = 0;
|
|
494
|
+
let state = "prompt";
|
|
495
|
+
while (true) {
|
|
496
|
+
if (state === "prompt") {
|
|
497
|
+
if (entersSent >= 2)
|
|
498
|
+
throw new LoginCodeError(LOGIN_CODE_NOT_ACCEPTED);
|
|
499
|
+
await ops.sendEnter();
|
|
500
|
+
entersSent += 1;
|
|
501
|
+
}
|
|
502
|
+
else if (state === "continue") {
|
|
503
|
+
// ログインは完了している。継続待ちを Enter で閉じて入力欄へ戻す(最大 2 回)。閉じられなくても
|
|
504
|
+
// 失敗にはしない(iOS 側は成功画面を「続ける」カードとして転写できる)。
|
|
505
|
+
if (continueEnters >= 2)
|
|
506
|
+
return;
|
|
507
|
+
await ops.sendEnter();
|
|
508
|
+
continueEnters += 1;
|
|
509
|
+
}
|
|
510
|
+
const attemptDeadline = now() + ops.settleMs;
|
|
511
|
+
for (;;) {
|
|
512
|
+
await sleep(ops.pollMs);
|
|
513
|
+
const screen = await ops.capture();
|
|
514
|
+
state = screen === null ? "pending" : loginCodeScreenState(screen);
|
|
515
|
+
if (state === "retry") {
|
|
516
|
+
const reason = screen === null ? null : loginCodeErrorLine(screen);
|
|
517
|
+
throw new LoginCodeError(`コードが拒否されました${reason !== null ? `(${reason})` : ""}。もう一度サインインしてください`);
|
|
518
|
+
}
|
|
519
|
+
if (state === "method") {
|
|
520
|
+
throw new LoginCodeError("ログインが中断され、方式選択に戻りました。/login をやり直してください");
|
|
521
|
+
}
|
|
522
|
+
if (state === "accepted")
|
|
523
|
+
return;
|
|
524
|
+
if (state === "continue")
|
|
525
|
+
break;
|
|
526
|
+
if (state === "prompt" && now() >= attemptDeadline)
|
|
527
|
+
break;
|
|
528
|
+
if (state === "pending" && now() >= pendingDeadline) {
|
|
529
|
+
// 継続待ちを閉じた後の描画遅延なら成功として扱う(継続画面を一度でも見ている)。
|
|
530
|
+
if (continueEnters > 0)
|
|
531
|
+
return;
|
|
532
|
+
throw new LoginCodeError(LOGIN_CODE_UNSETTLED);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
111
537
|
/** 入力欄の行群を「モード記号 + 本文」へ分解する。 */
|
|
112
538
|
function splitInputPrompt(bodyLines) {
|
|
113
539
|
const body = [...bodyLines];
|
|
@@ -159,12 +585,22 @@ export class TmuxSessionManager {
|
|
|
159
585
|
runner;
|
|
160
586
|
store;
|
|
161
587
|
captureLines;
|
|
588
|
+
/** login-code 送出の待ち時間(テスト注入用)。 */
|
|
589
|
+
loginTiming;
|
|
162
590
|
protocolVersion;
|
|
591
|
+
/** clearInputBox の C-u 1回ごとの反映待ち ms(herdr 側と同じ既定 150ms。テスト注入用)。 */
|
|
592
|
+
clearKeyDelayMs;
|
|
163
593
|
constructor(options = {}) {
|
|
164
594
|
this.runner = options.runner ?? processTmuxCommandRunner();
|
|
165
595
|
this.store = options.store ?? new SessionMetadataStore();
|
|
166
596
|
this.captureLines = options.captureLines ?? 50;
|
|
167
597
|
this.protocolVersion = options.protocolVersion ?? PROTOCOL_V1;
|
|
598
|
+
this.clearKeyDelayMs = options.clearKeyDelayMs ?? 150;
|
|
599
|
+
this.loginTiming = {
|
|
600
|
+
delayMs: options.loginTiming?.delayMs ?? 150,
|
|
601
|
+
pollMs: options.loginTiming?.pollMs ?? 250,
|
|
602
|
+
settleMs: options.loginTiming?.settleMs ?? 5_000,
|
|
603
|
+
};
|
|
168
604
|
}
|
|
169
605
|
/**
|
|
170
606
|
* 現存する各セッションを name/cwd/alive で列挙する(name 昇順、メタのみは alive:false)。
|
|
@@ -174,8 +610,14 @@ export class TmuxSessionManager {
|
|
|
174
610
|
*/
|
|
175
611
|
async list() {
|
|
176
612
|
const alive = await this.liveSessionNames();
|
|
613
|
+
const allMetas = this.store.all();
|
|
177
614
|
// herdr backend のメタは HerdrSessionManager が列挙する(Composite で和を取る)。
|
|
178
|
-
const metas =
|
|
615
|
+
const metas = allMetas.filter((meta) => meta.backend !== "herdr");
|
|
616
|
+
// herdr 担当の名前は tmux の生存集合からも外す。backend 切替前の同名 tmux セッションが
|
|
617
|
+
// 生き残っている間(reaper の idle 回収まで)、ここが backend:"tmux" を確定申告すると
|
|
618
|
+
// Composite の tmux 優先マージで正しい herdr 行(メタ = ルーティング権威)が消え、
|
|
619
|
+
// iOS の観測が tmux に固定される(cwd/会話 id も欠けて稼働中ピルの join からも落ちる)。
|
|
620
|
+
const herdrNames = new Set(allMetas.filter((meta) => meta.backend === "herdr").map((meta) => meta.name));
|
|
179
621
|
const cwdByName = new Map();
|
|
180
622
|
const claudeSessionIdByName = new Map();
|
|
181
623
|
const providerSessionIdByName = new Map();
|
|
@@ -191,13 +633,16 @@ export class TmuxSessionManager {
|
|
|
191
633
|
if (providerSessionId !== undefined)
|
|
192
634
|
providerSessionIdByName.set(meta.name, providerSessionId);
|
|
193
635
|
}
|
|
194
|
-
const names = new Set(alive);
|
|
636
|
+
const names = new Set([...alive].filter((name) => !herdrNames.has(name)));
|
|
195
637
|
for (const meta of metas)
|
|
196
638
|
names.add(meta.name);
|
|
639
|
+
// backend は tmux も常に明示する。iOS は欄なしを「host 未申告」として既知の観測値を
|
|
640
|
+
// 保持するため、欄の省略を tmux の意味に使わない(session-backend)。
|
|
197
641
|
const infos = [...names].map((name) => ({
|
|
198
642
|
name,
|
|
199
643
|
cwd: cwdByName.get(name) ?? "",
|
|
200
644
|
alive: alive.has(name),
|
|
645
|
+
backend: "tmux",
|
|
201
646
|
...(claudeSessionIdByName.has(name) ? { claudeSessionId: claudeSessionIdByName.get(name) } : {}),
|
|
202
647
|
...(agentByName.has(name) ? { agent: agentByName.get(name) } : {}),
|
|
203
648
|
...(providerSessionIdByName.has(name)
|
|
@@ -238,7 +683,7 @@ export class TmuxSessionManager {
|
|
|
238
683
|
}
|
|
239
684
|
const cwd = this.store.get(name)?.cwd ?? "";
|
|
240
685
|
const recent = await this.capturePane(name);
|
|
241
|
-
return { kind: "attached", info: { name, cwd, alive: true }, recentOutput: recent };
|
|
686
|
+
return { kind: "attached", info: { name, cwd, alive: true, backend: "tmux" }, recentOutput: recent };
|
|
242
687
|
}
|
|
243
688
|
/** pane 内のエージェント生存判定。tmux エラーや空出力は二重起動を避けて true に倒す。 */
|
|
244
689
|
async agentProcessAlive(name) {
|
|
@@ -275,14 +720,37 @@ export class TmuxSessionManager {
|
|
|
275
720
|
* 本文入力と送信確定を 1 操作で行う(chat 注入・kick 用, SessionBackend 共通面)。
|
|
276
721
|
* literal 送出 → 150ms(Ink 再描画待ち)→ Enter。
|
|
277
722
|
*/
|
|
278
|
-
async sendTextSubmit(name, text) {
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
723
|
+
async sendTextSubmit(name, text, options = {}) {
|
|
724
|
+
// 1 回の capture で「/login フロー中か」と「残存テキスト」を判定する(chat 毎の capture を増やさない)。
|
|
725
|
+
const screen = await this.captureVisibleScreenOrNull(name);
|
|
726
|
+
// `/login` フロー中(方式選択 / コード入力待ち / retry)は通常の入力欄が無く、注入した本文が
|
|
727
|
+
// 選択リストやコード欄へ入る。明示エラーで chat_send を失敗させる(コードは login_code_send、
|
|
728
|
+
// 再試行/中断は pane_key_send の専用経路)。
|
|
729
|
+
if (screen !== null && screenInLoginFlow(screen)) {
|
|
730
|
+
throw new LoginCodeError(CHAT_BLOCKED_BY_LOGIN_PROMPT);
|
|
731
|
+
}
|
|
732
|
+
// 中断(停止)直後の入力欄に残存テキストがあるまま注入すると、今回の本文がその後ろへ連結され
|
|
733
|
+
// 1 メッセージになる(実機FB 2026-07-29)。残存の身元で扱いを分ける(herdr 側と同じ規則):
|
|
734
|
+
// - transcript の直近の発話と同文 = 出力前の中断で claude が書き戻した「配送済みの発話」
|
|
735
|
+
// → 破棄(C-u)。Enter で送り直すと同じ発話が二重に届く(実機 2026-09-08)。
|
|
736
|
+
// - それ以外(旧版の queued 書き戻し / Mac 側の下書き)→ 従来どおり Enter で独立送信し切る。
|
|
737
|
+
// 判定は ANSI で行い、薄字(faint)のプロンプト提案/プレースホルダーを実テキストと
|
|
738
|
+
// 数えない(実障害 2026-09-03: 提案を残留と誤認し Enter で勝手に送信していた)。
|
|
739
|
+
const ansiScreen = await this.captureVisibleScreenAnsiOrNull(name);
|
|
740
|
+
const pending = ansiScreen !== null && inputBoxHasRealPendingText(ansiScreen)
|
|
741
|
+
? (inputBoxRealText(ansiScreen) ?? "")
|
|
742
|
+
: "";
|
|
743
|
+
if (pending.length > 0) {
|
|
744
|
+
const recorded = options.recordedPromptText?.() ?? null;
|
|
745
|
+
if (recorded !== null && inputBoxTextMatchesRecordedPrompt(pending, recorded)) {
|
|
746
|
+
if (!(await this.clearInputBox(name))) {
|
|
747
|
+
throw new TmuxFailedError(["send-keys", "-t", this.paneTarget(name), "C-u"], 1, "restored prompt could not be cleared from the input box (中断で書き戻された発話が残存)");
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
await this.sendKeys(name, ["Enter"]);
|
|
752
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
753
|
+
}
|
|
286
754
|
}
|
|
287
755
|
// 入力欄がシェルモード(プロンプト `!`)のまま残っていると、注入した通常メッセージが
|
|
288
756
|
// そのままシェルコマンドとして実行される。空入力の Backspace(tmux キー名は BSpace)で
|
|
@@ -293,6 +761,31 @@ export class TmuxSessionManager {
|
|
|
293
761
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
294
762
|
await this.sendKeys(name, ["Enter"]);
|
|
295
763
|
}
|
|
764
|
+
/**
|
|
765
|
+
* `/login` の OAuth コードを入力欄へ渡して確定する(login_code_send)。
|
|
766
|
+
* コード入力待ちの画面でなければ何も送らず throw する(誤って通常入力欄へ
|
|
767
|
+
* コードが本文として送信されるのを防ぐ)。literal 送出 → 150ms → Enter は
|
|
768
|
+
* sendTextSubmit と同じ Ink の取り込み間隔。
|
|
769
|
+
*/
|
|
770
|
+
async sendLoginCode(name, code) {
|
|
771
|
+
await submitLoginCode(code, {
|
|
772
|
+
capture: () => this.captureVisibleScreenOrNull(name),
|
|
773
|
+
sendLiteral: (text) => this.sendKeys(name, [text], true),
|
|
774
|
+
sendEnter: () => this.sendKeys(name, ["Enter"]),
|
|
775
|
+
delayMs: this.loginTiming.delayMs,
|
|
776
|
+
pollMs: this.loginTiming.pollMs,
|
|
777
|
+
settleMs: this.loginTiming.settleMs,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
/** 画面キャプチャ(判定不能=capture 失敗は null)。 */
|
|
781
|
+
async captureVisibleScreenOrNull(name) {
|
|
782
|
+
try {
|
|
783
|
+
return await this.captureVisibleScreen(name);
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
296
789
|
/**
|
|
297
790
|
* 入力欄がシェルモードなら BSpace で通常入力へ戻す。判定不能・tmux エラーは no-op
|
|
298
791
|
* (fail-open。この補助操作のエラーを表に出すと「送信できない」原因が BSpace 送出の
|
|
@@ -312,11 +805,28 @@ export class TmuxSessionManager {
|
|
|
312
805
|
// no-op(fail-open)
|
|
313
806
|
}
|
|
314
807
|
}
|
|
315
|
-
/**
|
|
316
|
-
|
|
808
|
+
/**
|
|
809
|
+
* 入力欄を空にする(中断で書き戻された配送済み発話の破棄用, restored-prompt-discard)。
|
|
810
|
+
* C-u(kill-line) を繰り返す(herdr 側 clearInputBox と同じ規則: 多行本文は末尾行→改行の順に
|
|
811
|
+
* 消え 2N-1 回で必ず空になる。Backspace は本文の実文字を削るため使わない)。空にできたら true。
|
|
812
|
+
* 入力欄が見えない / 上限回数で空にならない / capture 失敗は false(呼び出し側は注入を諦めて
|
|
813
|
+
* 明示再送へ倒す。残存の上へ重ね打ちして連結送信するよりよい)。
|
|
814
|
+
*/
|
|
815
|
+
async clearInputBox(name) {
|
|
317
816
|
try {
|
|
318
|
-
|
|
319
|
-
|
|
817
|
+
for (let attempt = 0; attempt < 15; attempt += 1) {
|
|
818
|
+
const ansiScreen = await this.captureVisibleScreenAnsiOrNull(name);
|
|
819
|
+
const realText = ansiScreen === null ? null : inputBoxRealText(ansiScreen);
|
|
820
|
+
if (realText === null)
|
|
821
|
+
return false;
|
|
822
|
+
if (realText.replace(/\s+/g, "").length === 0)
|
|
823
|
+
return true;
|
|
824
|
+
await this.sendKeys(name, ["C-u"]);
|
|
825
|
+
await new Promise((resolve) => setTimeout(resolve, this.clearKeyDelayMs));
|
|
826
|
+
}
|
|
827
|
+
const ansiScreen = await this.captureVisibleScreenAnsiOrNull(name);
|
|
828
|
+
const realText = ansiScreen === null ? null : inputBoxRealText(ansiScreen);
|
|
829
|
+
return realText !== null && realText.replace(/\s+/g, "").length === 0;
|
|
320
830
|
}
|
|
321
831
|
catch {
|
|
322
832
|
return false;
|
|
@@ -343,14 +853,53 @@ export class TmuxSessionManager {
|
|
|
343
853
|
}
|
|
344
854
|
return lines.join("\n");
|
|
345
855
|
}
|
|
856
|
+
/**
|
|
857
|
+
* viewport 全体を ANSI エスケープ付き(`capture-pane -e`)で取る。faint(SGR 2)属性で
|
|
858
|
+
* プロンプト提案/プレースホルダーを実テキストと見分けるのに使う(inputBoxHasRealPendingText)。
|
|
859
|
+
* 判定不能=capture 失敗は null(fail-open)。
|
|
860
|
+
*/
|
|
861
|
+
async captureVisibleScreenAnsiOrNull(name) {
|
|
862
|
+
// 判定不能は fail-open で null(残留 flush 判定を止めない)。exitCode≠0 だけでなく
|
|
863
|
+
// runner の reject(spawn 失敗など)も握る(text 側 captureVisibleScreenOrNull と同じ約束)。
|
|
864
|
+
try {
|
|
865
|
+
const args = ["capture-pane", "-p", "-e", "-t", this.paneTarget(name)];
|
|
866
|
+
const result = await this.runner(args);
|
|
867
|
+
if (result.exitCode !== 0)
|
|
868
|
+
return null;
|
|
869
|
+
const lines = result.stdout.split("\n");
|
|
870
|
+
while (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() === "") {
|
|
871
|
+
lines.pop();
|
|
872
|
+
}
|
|
873
|
+
return lines.join("\n");
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
/** SessionBackend: プロンプト提案抽出用の viewport ANSI キャプチャ(失敗は throw)。 */
|
|
880
|
+
async captureVisibleAnsi(name) {
|
|
881
|
+
validateSessionName(name);
|
|
882
|
+
const args = ["capture-pane", "-p", "-e", "-t", this.paneTarget(name)];
|
|
883
|
+
const result = await this.runner(args);
|
|
884
|
+
if (result.exitCode !== 0) {
|
|
885
|
+
throw new TmuxFailedError(args, result.exitCode, result.stderr);
|
|
886
|
+
}
|
|
887
|
+
const lines = result.stdout.split("\n");
|
|
888
|
+
while (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() === "") {
|
|
889
|
+
lines.pop();
|
|
890
|
+
}
|
|
891
|
+
return lines.join("\n");
|
|
892
|
+
}
|
|
346
893
|
/** 指定セッションの pane へ tmux send-keys を発行する(literal は -l)。 */
|
|
347
894
|
async sendKeys(name, keys, literal = false) {
|
|
348
895
|
validateSessionName(name);
|
|
349
896
|
if (keys.length === 0)
|
|
350
897
|
return;
|
|
351
898
|
const args = ["send-keys", "-t", this.paneTarget(name)];
|
|
899
|
+
// literal は `--` で引数終端を明示する(先頭が `-` の本文 — base64url の OAuth コード等 —
|
|
900
|
+
// を tmux がフラグと誤認して `unknown flag` で失敗する)。
|
|
352
901
|
if (literal)
|
|
353
|
-
args.push("-l");
|
|
902
|
+
args.push("-l", "--");
|
|
354
903
|
args.push(...keys);
|
|
355
904
|
const result = await this.runner(args);
|
|
356
905
|
if (result.exitCode !== 0) {
|