tailii-host 0.10.1 → 0.11.1
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.d.ts +2 -0
- package/dist/approval/hook.js +38 -3
- package/dist/approval/hook.js.map +1 -1
- package/dist/backend/herdr.d.ts +71 -23
- package/dist/backend/herdr.js +277 -75
- package/dist/backend/herdr.js.map +1 -1
- package/dist/backend/sessionBackend.d.ts +8 -1
- package/dist/backend/sessionBackend.js +3 -0
- package/dist/backend/sessionBackend.js.map +1 -1
- package/dist/backend/tmux.d.ts +387 -2
- package/dist/backend/tmux.js +936 -55
- package/dist/backend/tmux.js.map +1 -1
- package/dist/chat/subagentTranscript.js +11 -3
- package/dist/chat/subagentTranscript.js.map +1 -1
- package/dist/chat/transcriptTailer.d.ts +3 -1
- package/dist/chat/transcriptTailer.js +65 -4
- package/dist/chat/transcriptTailer.js.map +1 -1
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.js +59 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/launch.d.ts +14 -0
- package/dist/commands/launch.js +84 -24
- package/dist/commands/launch.js.map +1 -1
- package/dist/engine/engine.js +1 -0
- package/dist/engine/engine.js.map +1 -1
- package/dist/engine/handlers/conversation.js +29 -2
- package/dist/engine/handlers/conversation.js.map +1 -1
- package/dist/hub/hubDaemon.js +18 -5
- package/dist/hub/hubDaemon.js.map +1 -1
- package/dist/hub/hubProtocol.d.ts +16 -0
- package/dist/hub/hubProtocol.js +26 -2
- package/dist/hub/hubProtocol.js.map +1 -1
- package/dist/hub/reaper.d.ts +31 -0
- package/dist/hub/reaper.js +122 -4
- package/dist/hub/reaper.js.map +1 -1
- package/dist/hub/sessionHub.d.ts +76 -0
- package/dist/hub/sessionHub.js +177 -12
- package/dist/hub/sessionHub.js.map +1 -1
- package/dist/protocol/decode.js +11 -0
- package/dist/protocol/decode.js.map +1 -1
- package/dist/protocol/messages.d.ts +30 -0
- package/dist/sessions/claudeLiveStatus.d.ts +62 -0
- package/dist/sessions/claudeLiveStatus.js +202 -0
- package/dist/sessions/claudeLiveStatus.js.map +1 -0
- package/dist/sessions/claudeSessionStore.js +13 -0
- package/dist/sessions/claudeSessionStore.js.map +1 -1
- package/dist/sessions/cronSchedule.d.ts +20 -0
- package/dist/sessions/cronSchedule.js +172 -0
- package/dist/sessions/cronSchedule.js.map +1 -0
- package/dist/sessions/heartbeat.d.ts +44 -3
- package/dist/sessions/heartbeat.js +44 -4
- package/dist/sessions/heartbeat.js.map +1 -1
- package/dist/sessions/sessionSearch.js +9 -0
- package/dist/sessions/sessionSearch.js.map +1 -1
- package/dist/shared/clientBuild.d.ts +1 -1
- package/dist/shared/clientBuild.js +1 -1
- package/dist/shared/crossSession.d.ts +10 -3
- package/dist/shared/crossSession.js +105 -1
- package/dist/shared/crossSession.js.map +1 -1
- package/dist/shared/localCommandModelCopy.d.ts +2 -0
- package/dist/shared/localCommandModelCopy.js +43 -0
- package/dist/shared/localCommandModelCopy.js.map +1 -0
- package/dist/shared/narration.d.ts +7 -0
- package/dist/shared/narration.js +119 -0
- package/dist/shared/narration.js.map +1 -0
- package/dist/shared/pastedContent.d.ts +7 -0
- package/dist/shared/pastedContent.js +113 -0
- package/dist/shared/pastedContent.js.map +1 -0
- package/dist/shared/systemNotice.js +4 -1
- package/dist/shared/systemNotice.js.map +1 -1
- package/package.json +3 -3
package/dist/backend/herdr.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
// tailii (TS host) — herdr backend のセッション操作(list / reattach / kill / send / read)。
|
|
3
3
|
// TmuxSessionManager と同じ面(SessionBackend)を herdr CLI(socket API のフロント)で実装する。
|
|
4
4
|
//
|
|
5
|
-
// 実測済みの herdr 0.7.5
|
|
5
|
+
// 実測済みの herdr 0.7.5 挙動(0.9.3 でも CLI 面・JSON 形状は同一。設計正本は docs/herdr-backend.md):
|
|
6
6
|
// - 全 pane は専用 named session `tailii` に収容(全コマンドに `--session tailii` 前置)。
|
|
7
7
|
// 起動は `tab create --cwd --env --no-focus`(root_pane.pane_id が新 pane)→ `pane rename`
|
|
8
|
-
// → `pane run <id> "exec /bin/zsh -lc '<cmd>'"
|
|
9
|
-
//
|
|
8
|
+
// → `pane run <id> "exec env -u HERDR_ENV /bin/zsh -lc '<cmd>'"`(長いときは一時スクリプト経由)。
|
|
9
|
+
// 0.7.5 で `agent start` から pane 生成/cwd/env 指定が廃止されたため(旧: agent start 一発で pane 起動)。
|
|
10
10
|
// `pane run` はタイプ注入で argv のクォートを保持しない → コマンドは単一文字列で渡す。
|
|
11
11
|
// exec 前置によりコマンド終了と同時に pane が閉じる(旧 agent start と同義・stale shell なし)。
|
|
12
12
|
// - `pane read --source recent-unwrapped --lines N` は tmux `capture-pane -J -S -N` 相当。
|
|
@@ -21,9 +21,8 @@ import * as fs from "node:fs";
|
|
|
21
21
|
import * as os from "node:os";
|
|
22
22
|
import * as path from "node:path";
|
|
23
23
|
import { PROTOCOL_V1 } from "../protocol.js";
|
|
24
|
-
import { normalizeForTextMatch, stripInvisibleForComparison } from "../shared/invisibleText.js";
|
|
25
24
|
import { HERDR_PANE_ID_PATTERN, SessionMetadataStore, validateSessionName, } from "../sessions/sessionMetadataStore.js";
|
|
26
|
-
import { CHAT_BLOCKED_BY_LOGIN_PROMPT, claudeInputBoxRendered, extractClaudeInputBox, inputBoxHasRealPendingText, inputBoxRealText, inputBoxIsShellMode, inputBoxTextMatchesRecordedPrompt, LoginCodeError, paneCommandLooksLikeAgent, screenInLoginFlow, ChatInjectionRejectedError, classifySubmitFrame, SUBMIT_ATTEMPT_LIMIT, submitLoginCode, } from "./tmux.js";
|
|
25
|
+
import { cancelChoiceDialog, CHAT_BLOCKED_BY_LOGIN_PROMPT, CHAT_BLOCKED_AFTER_CHOICE_CANCELLED, CHAT_BLOCKED_BY_STUCK_CHOICE, CHAT_BLOCKED_BY_UNREADABLE_CHOICE, CHOICE_CANCEL_MAX_POLLS, CHOICE_CANCEL_POLL_MS, claudeComposerBarVisible, claudeInputBoxRendered, extractClaudeInputBox, inputBoxHasRealPendingText, inputBoxRealText, inputBoxIsShellMode, inputBoxTextMatchesRecordedPrompt, LoginCodeError, paneCommandLooksLikeAgent, screenInLoginFlow, inputBoxTextIncludesProbe, typedTextProbe, typedTextWouldBreak, textIsPasteSafe, pasteTextInline, clearTypedInput, INJECT_VERIFY_BUDGET_MS, CLEAR_UNKNOWN_INPUT_BOUND, BRACKETED_PASTE_START, BRACKETED_PASTE_END, PASTED_TEXT_POLL_MS, PASTED_TEXT_TIMEOUT_MS, TYPED_TEXT_CLEAR_SETTLE_MS, ChatInjectionRejectedError, classifySubmitFrame, SEND_NOW_MAX_POLLS, SEND_NOW_POLL_MS, sendQueuedNow, settleFrameAfterChoiceCancel, SUBMIT_ATTEMPT_LIMIT, submitLoginCode, } from "./tmux.js";
|
|
27
26
|
/** herdr 実行ファイルの既定絶対パス(SSH exec は非ログインシェルで PATH 外のため絶対指定)。 */
|
|
28
27
|
export function defaultHerdrPath() {
|
|
29
28
|
return path.join(os.homedir(), ".local", "bin", "herdr");
|
|
@@ -56,6 +55,15 @@ const HERDR_TIMEOUT_MS = 15_000;
|
|
|
56
55
|
* (2026-09-06 実機で発症)。窓内のシェル前面は「起動中」として生存扱いにする。
|
|
57
56
|
*/
|
|
58
57
|
export const HERDR_LAUNCH_GRACE_SECONDS = 20;
|
|
58
|
+
/**
|
|
59
|
+
* TMUX / TMUX_PANE を除いた環境。herdr server は最初に `herdr` を叩いたプロセスの環境を引き継ぎ、
|
|
60
|
+
* その配下の Claude は TMUX があると外側の tmux pane 名を状態ファイルの `tmux` 欄に書く
|
|
61
|
+
* (reaper の照合を取り違える)。tmux 内の hook / CLI から起動されても持ち込まないよう外す。
|
|
62
|
+
*/
|
|
63
|
+
export function envWithoutTmux(env = process.env) {
|
|
64
|
+
const { TMUX: _tmux, TMUX_PANE: _tmuxPane, ...rest } = env;
|
|
65
|
+
return rest;
|
|
66
|
+
}
|
|
59
67
|
/**
|
|
60
68
|
* 実 herdr を起動する既定ランナー。herdr 非0 exit は throw せず結果で表現する。
|
|
61
69
|
* `sessionName`(既定 tailii)を `--session` として全コマンドに前置する。
|
|
@@ -66,7 +74,7 @@ export function processHerdrCommandRunner(herdrPath = defaultHerdrPath(), sessio
|
|
|
66
74
|
// 発行するのは pane list / pane get / tab list など即応するコマンドだけ。無期限に
|
|
67
75
|
// 待つと engine の read loop と hub の tick ループが同時に止まるため上限を切る
|
|
68
76
|
// (tmux ランナー・`gitService` と同じ規約)。
|
|
69
|
-
{ maxBuffer: 16 * 1024 * 1024, timeout: timeoutMs }, (error, stdout, stderr) => {
|
|
77
|
+
{ maxBuffer: 16 * 1024 * 1024, timeout: timeoutMs, env: envWithoutTmux() }, (error, stdout, stderr) => {
|
|
70
78
|
if (error && typeof error.code === "string") {
|
|
71
79
|
// 実行ファイル起動自体の失敗(ENOENT 等)のみ throw(tmux ランナーと同じ境界)。
|
|
72
80
|
reject(error);
|
|
@@ -77,13 +85,48 @@ export function processHerdrCommandRunner(herdrPath = defaultHerdrPath(), sessio
|
|
|
77
85
|
});
|
|
78
86
|
});
|
|
79
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* herdr CLI のエラー JSON(`{"error":{"code":"…"}}`、stderr に出る)から code を取り出す。
|
|
90
|
+
* 判定不能は null。
|
|
91
|
+
*/
|
|
92
|
+
export function herdrErrorCode(output) {
|
|
93
|
+
const text = output.trim();
|
|
94
|
+
if (text.length === 0)
|
|
95
|
+
return null;
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(text);
|
|
98
|
+
return typeof parsed.error?.code === "string" ? parsed.error.code : null;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 旧サーバーを止めて新しい版へ移る手順。`delete` まで行うのは、旧版の pane が herdr の
|
|
106
|
+
* claude 連携へ会話 id を申告済みで、新しいサーバー(≥0.8)が保存状態から復元するときに
|
|
107
|
+
* Tailii のフック抜きの `claude --resume` で自動再開してしまうため(実測 2026-09-30,
|
|
108
|
+
* 0.7.5 の保存状態 → 0.9.3 サーバー)。保存状態を消せば会話は開き直したときに Tailii が
|
|
109
|
+
* 自前の起動で resume する。0.7.5 の `pane release-agent` は申告を消さない(実測)。
|
|
110
|
+
*/
|
|
111
|
+
export const HERDR_RESTART_COMMAND = "herdr session stop tailii && herdr session delete tailii";
|
|
112
|
+
/**
|
|
113
|
+
* herdr を更新した後、tailii セッションのサーバーが旧版のまま動いている(`protocol_mismatch`)
|
|
114
|
+
* ときの案内。0.9.x の CLI は 0.7.5 のサーバーへ全コマンドを拒否するため、サーバーを
|
|
115
|
+
* 止めるまで Tailii の herdr 操作は全て失敗する(実測 2026-09-30, 0.9.3 CLI × 0.7.5 server)。
|
|
116
|
+
* 止めると pane のプロセス(実行中の会話)は終了するので、自動では止めず利用者に委ねる。
|
|
117
|
+
*/
|
|
118
|
+
export const HERDR_RESTART_NEEDED_MESSAGE = "herdr が更新されましたが、Tailii 用の herdr サーバーは古い版のまま動いています。" +
|
|
119
|
+
`Mac で \`${HERDR_RESTART_COMMAND}\` を実行してから会話を開き直してください` +
|
|
120
|
+
"(実行中の会話は一度終了し、開き直すと続きから再開します)。";
|
|
80
121
|
/** HerdrSessionManager が投げる型付きエラー。 */
|
|
81
122
|
export class HerdrFailedError extends Error {
|
|
82
123
|
args;
|
|
83
124
|
exitCode;
|
|
84
125
|
stderr;
|
|
85
126
|
constructor(args, exitCode, stderr) {
|
|
86
|
-
super(
|
|
127
|
+
super(herdrErrorCode(stderr) === "protocol_mismatch"
|
|
128
|
+
? HERDR_RESTART_NEEDED_MESSAGE
|
|
129
|
+
: `herdr ${args.join(" ")} failed (exit ${exitCode}): ${stderr}`);
|
|
87
130
|
this.args = args;
|
|
88
131
|
this.exitCode = exitCode;
|
|
89
132
|
this.stderr = stderr;
|
|
@@ -121,6 +164,12 @@ const ENTER_SEQUENCE = "\r";
|
|
|
121
164
|
* 空 composer への C-u は完全に no-op で、処理中の agent にも干渉しない)。
|
|
122
165
|
*/
|
|
123
166
|
const KILL_LINE_SEQUENCE = "\u0015";
|
|
167
|
+
/**
|
|
168
|
+
* `chat:sendNow` の chord(Ctrl-X Ctrl-S)の生制御文字(chat-send-now)。herdr の send-keys は
|
|
169
|
+
* キーごとに別プロセスで送るため、2 打の間が空くと chord が成立しない恐れがある。
|
|
170
|
+
* **1 回の send-text で続けて送る**(実測 herdr 0.7.5 + Claude Code 2.1.283 で成立)。
|
|
171
|
+
*/
|
|
172
|
+
const SEND_NOW_SEQUENCE = "\u0018\u0013";
|
|
124
173
|
/**
|
|
125
174
|
* 画面に選択ダイアログのフッター行があるか。チャット本文が「Enter to select」を
|
|
126
175
|
* 引用しただけの偽陽性(誤 Esc・再送スキップの実害)を避けるため、substring では
|
|
@@ -138,48 +187,8 @@ function stripAnsiForMatch(line) {
|
|
|
138
187
|
// eslint-disable-next-line no-control-regex
|
|
139
188
|
return line.replace(/\u001b\[[0-9;:?]*[ -/]*[@-~]/g, "");
|
|
140
189
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
*
|
|
144
|
-
* 末尾側なのは composer の表示特性のため: 入力が表示高を超えると composer は下へ
|
|
145
|
-
* スクロールし**先頭行が窓外へ消える**(実測 2.1.220: 10行ペーストで先頭4行が
|
|
146
|
-
* capture から消失)。カーソルは常に末尾にあるので、末尾側の probe だけが
|
|
147
|
-
* 「見えている範囲」との照合を保証できる(先頭24字の旧 probe は多行/長文で
|
|
148
|
-
* 構造的に偽陰性 → 再投入 → 本文二重化の温床だった)。
|
|
149
|
-
*
|
|
150
|
-
* 先頭 `!`(シェルモード)は claude TUI がモード記号として吸い上げ、入力欄本文には
|
|
151
|
-
* 残らない(`!ls -la` は `! ls -la` と描画される)。単一行本文では照合キーからも
|
|
152
|
-
* 落とさないと末尾24字に `!` が含まれるとき反映検証が失敗する。
|
|
153
|
-
*/
|
|
154
|
-
export function typedTextProbe(text) {
|
|
155
|
-
// 不可視文字は端末のセルに載らない(capture に出ない)ため、probe に混ざると反映検証が
|
|
156
|
-
// 構造的に偽陰性になる。probe は不可視文字を除いた可視本文から取る
|
|
157
|
-
// (照合側 `inputBoxTextIncludesProbe` も両辺から落とすので規則は一致する)。
|
|
158
|
-
const lines = stripInvisibleForComparison(text)
|
|
159
|
-
.split("\n")
|
|
160
|
-
.map((line) => line.trim())
|
|
161
|
-
.filter((line) => line.length > 0);
|
|
162
|
-
const lastLine = lines.at(-1);
|
|
163
|
-
if (lastLine === undefined)
|
|
164
|
-
return null;
|
|
165
|
-
const body = lines.length === 1 && lastLine.startsWith("!") ? lastLine.slice(1).trim() : lastLine;
|
|
166
|
-
if (body.length === 0)
|
|
167
|
-
return null;
|
|
168
|
-
return body.slice(-24);
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* 入力欄テキストに probe が反映されているか(折り返し非依存)。
|
|
172
|
-
* extractClaudeInputBox は表示行を trim + "\n" 連結で返すため、probe が入力欄の
|
|
173
|
-
* 行折り返しをまたぐと生の includes は絶対に一致しない(全角24字=48桁 > 内幅で必発)。
|
|
174
|
-
* この偽陰性が「反映済み本文の再投入 = 初回送信の本文二重化」の根因だった(実機5件)。
|
|
175
|
-
* 空白類(折り返しの改行・trim 痕・全角空白含む)と不可視文字を両辺から除去して照合する。
|
|
176
|
-
*/
|
|
177
|
-
export function inputBoxTextIncludesProbe(boxText, probe) {
|
|
178
|
-
const needle = normalizeForTextMatch(probe);
|
|
179
|
-
if (needle.length === 0)
|
|
180
|
-
return false;
|
|
181
|
-
return normalizeForTextMatch(boxText).includes(needle);
|
|
182
|
-
}
|
|
190
|
+
// 反映検証の probe は tmux backend と共有する(long-text-paste)。従来の import 元を保つ。
|
|
191
|
+
export { inputBoxTextIncludesProbe, typedTextProbe } from "./tmux.js";
|
|
183
192
|
/** herdr CLI の JSON stdout から `result` を取り出す。JSON でない/エラー封筒は null。 */
|
|
184
193
|
export function parseHerdrResult(stdout) {
|
|
185
194
|
try {
|
|
@@ -312,6 +321,55 @@ export function parseHerdrForegroundCommand(stdout) {
|
|
|
312
321
|
const name = first["name"];
|
|
313
322
|
return typeof name === "string" ? name : "";
|
|
314
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* `pane process-info` の前面プロセス群の pid(前面プロセスグループ id を含む)。
|
|
326
|
+
* Claude Code の `~/.claude/sessions/<pid>.json` と照合する(tmux 欄の無い herdr 用, 実測で一致)。
|
|
327
|
+
*/
|
|
328
|
+
export function parseHerdrForegroundPids(stdout) {
|
|
329
|
+
const result = parseHerdrResult(stdout);
|
|
330
|
+
const info = result?.["process_info"];
|
|
331
|
+
if (typeof info !== "object" || info === null)
|
|
332
|
+
return [];
|
|
333
|
+
const raw = info;
|
|
334
|
+
const pids = new Set();
|
|
335
|
+
const group = raw["foreground_process_group_id"];
|
|
336
|
+
if (typeof group === "number" && Number.isInteger(group) && group > 0)
|
|
337
|
+
pids.add(group);
|
|
338
|
+
const list = raw["foreground_processes"];
|
|
339
|
+
if (Array.isArray(list)) {
|
|
340
|
+
for (const item of list) {
|
|
341
|
+
if (typeof item !== "object" || item === null)
|
|
342
|
+
continue;
|
|
343
|
+
const pid = item["pid"];
|
|
344
|
+
if (typeof pid === "number" && Number.isInteger(pid) && pid > 0)
|
|
345
|
+
pids.add(pid);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return [...pids];
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* セッション名に対応する pane を選ぶ。
|
|
352
|
+
* 1. 記録済み pane ID の pane が同じ label(または label なし)ならそれ。
|
|
353
|
+
* 2. 無ければ label(= セッション名)一致の pane。
|
|
354
|
+
* 3. それも無ければ、記録 ID の pane の label が**他の Tailii セッション名でない**限りそれ
|
|
355
|
+
* (利用者が herdr 上で pane 名を変えた会話も見失わない。`keys.rename_pane` で変えられる)。
|
|
356
|
+
* 記録 ID の pane が別の Tailii セッション名を持つときだけ採用しない: pane ID はサーバーの状態を
|
|
357
|
+
* 消したとき(`herdr session delete tailii` 後の初回起動など)に w1:p1 から振り直されるため、
|
|
358
|
+
* 古い記録 ID が別の会話の pane を指し得る(取り違えると別会話へ送信・stale 掃除で別会話を閉じる)。
|
|
359
|
+
* @param isTailiiName label がメタに記録された Tailii セッション名か(取り違えの判定用)。
|
|
360
|
+
* 3. に入ったときだけ呼ぶ(メタ全件の読み込みは重いので、通常の 1. / 2. では読まない)。
|
|
361
|
+
*/
|
|
362
|
+
export function resolveHerdrSessionPane(panes, name, recordedPaneId, isTailiiName) {
|
|
363
|
+
const byId = recordedPaneId !== undefined ? panes.find((pane) => pane.paneId === recordedPaneId) : undefined;
|
|
364
|
+
if (byId !== undefined && (byId.label === null || byId.label === name))
|
|
365
|
+
return byId;
|
|
366
|
+
const byLabel = panes.find((pane) => pane.label === name);
|
|
367
|
+
if (byLabel !== undefined)
|
|
368
|
+
return byLabel;
|
|
369
|
+
if (byId !== undefined && byId.label !== null && !isTailiiName(byId.label))
|
|
370
|
+
return byId;
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
315
373
|
/** herdr backend のセッション list / reattach / kill / send / read とメタデータ統合。 */
|
|
316
374
|
export class HerdrSessionManager {
|
|
317
375
|
runner;
|
|
@@ -322,6 +380,14 @@ export class HerdrSessionManager {
|
|
|
322
380
|
submitDelayMs;
|
|
323
381
|
/** sendTextSubmit の CR→残留確認 間隔 ms(テスト注入用)。 */
|
|
324
382
|
submitVerifyDelayMs;
|
|
383
|
+
/** 選択ダイアログを Esc で閉じた後の消滅確認の間隔 ms(テスト注入用)。 */
|
|
384
|
+
choiceCancelPollMs;
|
|
385
|
+
/** 「今すぐ送信」の chord 後、ヒント行の消滅確認の間隔 ms(テスト注入用)。 */
|
|
386
|
+
sendNowPollMs;
|
|
387
|
+
/** 貼り付けた本文が入力欄に入るのを確かめる間隔と上限 ms(テスト注入用)。 */
|
|
388
|
+
pastedTextPollMs;
|
|
389
|
+
pastedTextTimeoutMs;
|
|
390
|
+
clearSettleMs;
|
|
325
391
|
/** 入力欄へ本文が反映されなかったときの再投入間隔 ms(RC 切断 limbo 対策)。 */
|
|
326
392
|
inputRetryDelayMs;
|
|
327
393
|
/** clearInputBox の C-u 1回ごとの反映待ち ms(実測レイテンシ 9〜27ms。テスト注入用)。 */
|
|
@@ -343,6 +409,11 @@ export class HerdrSessionManager {
|
|
|
343
409
|
this.protocolVersion = options.protocolVersion ?? PROTOCOL_V1;
|
|
344
410
|
this.submitDelayMs = options.submitDelayMs ?? 600;
|
|
345
411
|
this.submitVerifyDelayMs = options.submitVerifyDelayMs ?? 700;
|
|
412
|
+
this.choiceCancelPollMs = options.choiceCancelPollMs ?? CHOICE_CANCEL_POLL_MS;
|
|
413
|
+
this.sendNowPollMs = options.sendNowPollMs ?? SEND_NOW_POLL_MS;
|
|
414
|
+
this.pastedTextPollMs = options.pastedTextPollMs ?? PASTED_TEXT_POLL_MS;
|
|
415
|
+
this.pastedTextTimeoutMs = options.pastedTextTimeoutMs ?? PASTED_TEXT_TIMEOUT_MS;
|
|
416
|
+
this.clearSettleMs = options.clearSettleMs ?? TYPED_TEXT_CLEAR_SETTLE_MS;
|
|
346
417
|
this.inputRetryDelayMs = options.inputRetryDelayMs ?? 1_500;
|
|
347
418
|
this.clearKeyDelayMs = options.clearKeyDelayMs ?? 150;
|
|
348
419
|
this.readyTimeoutMs = options.readyTimeoutMs ?? 10_000;
|
|
@@ -382,21 +453,19 @@ export class HerdrSessionManager {
|
|
|
382
453
|
return this.listFromPanes(panes);
|
|
383
454
|
}
|
|
384
455
|
async listFromPanes(panes) {
|
|
385
|
-
const paneIds = new Set(panes.map((pane) => pane.paneId));
|
|
386
|
-
const labels = new Set(panes.map((pane) => pane.label).filter((label) => label !== null));
|
|
387
456
|
// タブラベル(Mac 側リネーム含む会話タイトル表示)を name→label で引けるようにする
|
|
388
457
|
// (session-title 逆方向同期: セッション名と異なるラベルを displayTitle として載せる)。
|
|
389
458
|
const tabLabels = await this.tabLabels();
|
|
459
|
+
const metas = this.store.all();
|
|
460
|
+
const tailiiNames = new Set(metas.map((meta) => meta.name));
|
|
390
461
|
const infos = [];
|
|
391
|
-
for (const meta of
|
|
462
|
+
for (const meta of metas) {
|
|
392
463
|
if (meta.backend !== "herdr")
|
|
393
464
|
continue;
|
|
394
465
|
const agent = meta.agent ?? "claude";
|
|
395
466
|
const providerSessionId = meta.providerSessionId ?? (agent === "claude" ? meta.claudeSessionId : undefined);
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
? panes.find((candidate) => candidate.paneId === meta.herdrPaneId)
|
|
399
|
-
: undefined) ?? panes.find((candidate) => candidate.label === meta.name);
|
|
467
|
+
const pane = resolveHerdrSessionPane(panes, meta.name, meta.herdrPaneId, (label) => tailiiNames.has(label));
|
|
468
|
+
const alive = pane !== null;
|
|
400
469
|
const tabLabel = pane?.tabId != null ? tabLabels.get(pane.tabId) : undefined;
|
|
401
470
|
// 自動適用ラベル(hub tick が書いた値 = meta.autoTabTitle)と既定連番は人為リネーム
|
|
402
471
|
// ではないため displayTitle に載せない(iOS が override として誤取り込みしないため)。
|
|
@@ -500,6 +569,24 @@ export class HerdrSessionManager {
|
|
|
500
569
|
return true;
|
|
501
570
|
}
|
|
502
571
|
}
|
|
572
|
+
/** pane の前面プロセスの pid 群。取得できなければ null(reaper の Claude 状態照合用)。 */
|
|
573
|
+
async agentProcessIds(name) {
|
|
574
|
+
validateSessionName(name);
|
|
575
|
+
const target = await this.paneTarget(name);
|
|
576
|
+
if (target === null)
|
|
577
|
+
return null;
|
|
578
|
+
try {
|
|
579
|
+
const result = await this.runner(["pane", "process-info", "--pane", target]);
|
|
580
|
+
if (result.exitCode !== 0)
|
|
581
|
+
return null;
|
|
582
|
+
// 空は「前面プロセスが無い」ではなく形式変更・一時失敗の疑い → 取得不能として扱う。
|
|
583
|
+
const pids = parseHerdrForegroundPids(result.stdout);
|
|
584
|
+
return pids.length > 0 ? pids : null;
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
503
590
|
/**
|
|
504
591
|
* メタの createdAt(launch 時刻。launcher は pane 再利用時に更新しない)が猶予窓内か。
|
|
505
592
|
* 未記録は窓外(従来どおりの判定)。時計の巻き戻りで経過が負になった場合も窓外に倒し、
|
|
@@ -522,7 +609,7 @@ export class HerdrSessionManager {
|
|
|
522
609
|
validateSessionName(name);
|
|
523
610
|
const pane = await this.findPane(name);
|
|
524
611
|
if (pane === null) {
|
|
525
|
-
throw
|
|
612
|
+
throw await this.missingPaneError(["tab", "rename", name]);
|
|
526
613
|
}
|
|
527
614
|
if (pane.tabId === null) {
|
|
528
615
|
throw new HerdrFailedError(["tab", "rename", name], 1, "tab id unavailable");
|
|
@@ -540,7 +627,7 @@ export class HerdrSessionManager {
|
|
|
540
627
|
validateSessionName(name);
|
|
541
628
|
const target = await this.paneTarget(name);
|
|
542
629
|
if (target === null) {
|
|
543
|
-
throw
|
|
630
|
+
throw await this.missingPaneError(["pane", "close", name]);
|
|
544
631
|
}
|
|
545
632
|
const args = ["pane", "close", target];
|
|
546
633
|
const result = await this.runner(args);
|
|
@@ -558,6 +645,7 @@ export class HerdrSessionManager {
|
|
|
558
645
|
* リトライで確定させる。submit 済みの空入力への Enter は no-op なので二重送信は起きない。
|
|
559
646
|
*/
|
|
560
647
|
async sendTextSubmit(name, text, options = {}) {
|
|
648
|
+
const startedAtMs = Date.now();
|
|
561
649
|
// ブート直後の注入は本文ごと TUI 初期化に破棄され得る(実測: 入力欄にも jsonl にも
|
|
562
650
|
// 残らない)。herdr の claude 検出(agent_status が unknown を抜けるまで)を注入の
|
|
563
651
|
// 準備完了ゲートにする。working(処理中の queue 入力)も注入可。判定不能は fail-open。
|
|
@@ -583,9 +671,29 @@ export class HerdrSessionManager {
|
|
|
583
671
|
}
|
|
584
672
|
// 選択ダイアログ(/remote-control 等)が開いたままだと本文がダイアログに食われる。
|
|
585
673
|
// 注入前に Esc で閉じてから入力欄へ流す(Mac 側で手動 Esc するのと同じ操作)。
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
674
|
+
// herdr は `cancelChoiceDialog` 指定の有無に関わらず閉じる(従来からの挙動。設問は hub が
|
|
675
|
+
// 保留で守るので、ここへ来るのは利用者が答えずに送ると決めた送信か ❯ メニューだけ)。
|
|
676
|
+
// Esc は 1 回だけ打ち、消えたのを確かめてから進む(chat-cancel-choice)。旧実装は 300ms
|
|
677
|
+
// 待つだけで確認せず、閉じ切る前に本文を打ってダイアログへ落とし得た。
|
|
678
|
+
// ボトムバーが見えているフレームでは撃たない: Esc は処理中の Claude を中断するキーでもあり、
|
|
679
|
+
// 30 行窓は会話本文(閉じたダイアログの転写や引用)を含むため。
|
|
680
|
+
let choiceStuck = false;
|
|
681
|
+
let choiceCancelled = false;
|
|
682
|
+
if (dialogWindow !== null) {
|
|
683
|
+
const outcome = await cancelChoiceDialog({
|
|
684
|
+
screen: dialogWindow,
|
|
685
|
+
capture: () => this.captureDialogWindow(name),
|
|
686
|
+
sendEscape: () => this.sendKeys(name, ["Escape"]),
|
|
687
|
+
detect: (window) => screenHasSelectionFooter(window) && !claudeComposerBarVisible(window),
|
|
688
|
+
pollMs: this.choiceCancelPollMs,
|
|
689
|
+
maxPolls: CHOICE_CANCEL_MAX_POLLS,
|
|
690
|
+
});
|
|
691
|
+
// 閉じたかどうかの最終判断は下のフレーム判定に任せる(30 行窓は会話本文を含むので、
|
|
692
|
+
// 本文に残った文言で「閉じていない」と見誤っても、ダイアログでなければ従来どおり送る)。
|
|
693
|
+
choiceStuck = outcome === "stuck";
|
|
694
|
+
choiceCancelled = outcome === "cancelled";
|
|
695
|
+
if (choiceCancelled)
|
|
696
|
+
options.onChoiceDialogCancelled?.();
|
|
589
697
|
}
|
|
590
698
|
// 中断(停止)直後の入力欄に残存テキストがあるまま注入すると、今回の本文がその後ろへ
|
|
591
699
|
// 連結され 1 メッセージとして送信される(実機FB 2026-07-29: 停止→送信で「前回本文+今回
|
|
@@ -603,8 +711,25 @@ export class HerdrSessionManager {
|
|
|
603
711
|
// ではないので上の Esc では閉じられない)。ここまで来てバーが見えないなら、本文を
|
|
604
712
|
// 1 キーも打たずに諦める。
|
|
605
713
|
// 見逃し側(未知フレーム)は従来どおり注入へ進む=送信が丸ごと不能にはならない。
|
|
606
|
-
|
|
714
|
+
// 閉じた直後は描画途中でボトムバーが欠けることがある。設問を閉じる指定の送信だけ、
|
|
715
|
+
// 入力欄が描画されるまで短く撮り直す(指定の無い送信は read 回数を増やさない)。
|
|
716
|
+
const { frame: injectFrame } = await settleFrameAfterChoiceCancel({
|
|
717
|
+
capture: () => this.captureVisibleScreenAnsiOrNull(name),
|
|
718
|
+
pollMs: this.choiceCancelPollMs,
|
|
719
|
+
maxPolls: choiceCancelled && options.cancelChoiceDialog === true ? CHOICE_CANCEL_MAX_POLLS : 0,
|
|
720
|
+
});
|
|
721
|
+
if (choiceCancelled && options.cancelChoiceDialog === true && injectFrame === "unknown") {
|
|
722
|
+
throw new ChatInjectionRejectedError(CHAT_BLOCKED_AFTER_CHOICE_CANCELLED);
|
|
723
|
+
}
|
|
724
|
+
if (options.cancelChoiceDialog === true && injectFrame === "unknown") {
|
|
725
|
+
// 設問がある前提の送信は fail-closed: 入力欄(ボトムバー)が見えたときだけ打つ。
|
|
726
|
+
// 指定の無い送信は従来どおり(未知フレームで送信が丸ごと不能にならないよう fail-open)。
|
|
727
|
+
throw new ChatInjectionRejectedError(CHAT_BLOCKED_BY_UNREADABLE_CHOICE);
|
|
728
|
+
}
|
|
607
729
|
if (injectFrame === "dialog") {
|
|
730
|
+
// Esc は打ったが閉じない。ここで本文を打つとダイアログへ落ちるので、何も打たずに諦める。
|
|
731
|
+
if (choiceStuck)
|
|
732
|
+
throw new ChatInjectionRejectedError(CHAT_BLOCKED_BY_STUCK_CHOICE);
|
|
608
733
|
throw new ChatInjectionRejectedError("ダイアログの表示中はメッセージを送信できません(アプリで選択肢に答えてから送り直してください)");
|
|
609
734
|
}
|
|
610
735
|
const pending = injectFrame === "pending" ? await this.inputBoxPendingText(name) : "";
|
|
@@ -636,6 +761,43 @@ export class HerdrSessionManager {
|
|
|
636
761
|
// chat_send を uncertain(アプリの明示再送)へ倒す。
|
|
637
762
|
const probe = typedTextProbe(text);
|
|
638
763
|
let typed = probe === null;
|
|
764
|
+
if (!typed && typedTextWouldBreak(text) && textIsPasteSafe(text)) {
|
|
765
|
+
// 1 回で打つと壊れる本文は、小分けの括弧付き貼り付けで渡す(long-text-paste)。1 回で打つと
|
|
766
|
+
// CLI が読み取りの境界ごとに貼り付けとして取り込み、入力欄が `[Pasted text #N]` になって
|
|
767
|
+
// 下の反映検証が必ず失敗する(実測 2.1.283: 983 字の送信が 3 回打ち直した末に失敗)。
|
|
768
|
+
const target = await this.paneTarget(name);
|
|
769
|
+
if (target === null) {
|
|
770
|
+
throw await this.missingPaneError(["pane", "send-text", name]);
|
|
771
|
+
}
|
|
772
|
+
const sendRaw = async (payload) => {
|
|
773
|
+
const args = ["pane", "send-text", target, payload];
|
|
774
|
+
const result = await this.runner(args);
|
|
775
|
+
if (result.exitCode !== 0)
|
|
776
|
+
throw new HerdrFailedError(args, result.exitCode, result.stderr);
|
|
777
|
+
};
|
|
778
|
+
const outcome = await pasteTextInline(text, {
|
|
779
|
+
// 1 つずつ書き込む。まとめて書き込むと、CLI が読み進めるのを待たされる(実測: 8KB を
|
|
780
|
+
// 1 回で書くと入り切るまで 4〜7 秒、20KB は 10 秒でも入り切らない。1 つずつなら 1 秒)。
|
|
781
|
+
paste: async (pieces) => {
|
|
782
|
+
for (const piece of pieces)
|
|
783
|
+
await sendRaw(`${BRACKETED_PASTE_START}${piece}${BRACKETED_PASTE_END}`);
|
|
784
|
+
},
|
|
785
|
+
capture: () => this.captureVisibleScreenAnsiOrNull(name),
|
|
786
|
+
sendKills: (count) => sendRaw(KILL_LINE_SEQUENCE.repeat(count)),
|
|
787
|
+
pollMs: this.pastedTextPollMs,
|
|
788
|
+
timeoutMs: this.pastedTextTimeoutMs,
|
|
789
|
+
clearDelayMs: this.clearKeyDelayMs,
|
|
790
|
+
clearSettleMs: this.clearSettleMs,
|
|
791
|
+
verifyDeadlineMs: startedAtMs + INJECT_VERIFY_BUDGET_MS,
|
|
792
|
+
});
|
|
793
|
+
if (outcome !== "arrived") {
|
|
794
|
+
// 打つ場合の反映検証と同じ扱い: 残骸を best-effort で掃除してから失敗として返す。
|
|
795
|
+
// 貼り直しはしない(CLI が止まっていただけなら、貼り直した分だけ後から重なって入る)。
|
|
796
|
+
await this.clearInputBox(name);
|
|
797
|
+
throw new HerdrFailedError(["pane", "send-text", name], 1, "pasted text did not reach the input box (RC limbo / ダイアログ表示中 / 入力欄不可視 / CLI の停止のいずれか)");
|
|
798
|
+
}
|
|
799
|
+
typed = true;
|
|
800
|
+
}
|
|
639
801
|
for (let attempt = 0; attempt < 3 && !typed; attempt += 1) {
|
|
640
802
|
if (attempt > 0) {
|
|
641
803
|
await new Promise((resolve) => setTimeout(resolve, this.inputRetryDelayMs));
|
|
@@ -671,6 +833,15 @@ export class HerdrSessionManager {
|
|
|
671
833
|
if ((await this.submitTypedText(name)) !== "submitted")
|
|
672
834
|
options.onUnconfirmedSubmit?.();
|
|
673
835
|
}
|
|
836
|
+
/** CLI のキューに溜まっている発話を今すぐ届ける(chat-send-now, SessionBackend 共通面)。 */
|
|
837
|
+
async sendQueuedNow(name) {
|
|
838
|
+
return sendQueuedNow({
|
|
839
|
+
capture: () => this.captureVisibleScreenAnsiOrNull(name),
|
|
840
|
+
sendChord: () => this.sendKeys(name, [SEND_NOW_SEQUENCE], true),
|
|
841
|
+
pollMs: this.sendNowPollMs,
|
|
842
|
+
maxPolls: SEND_NOW_MAX_POLLS,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
674
845
|
/**
|
|
675
846
|
* 入力済みの本文を Enter で送信し、成立を確認するまで上限つきで撃ち直す(tmux 側と同型)。
|
|
676
847
|
*
|
|
@@ -764,6 +935,7 @@ export class HerdrSessionManager {
|
|
|
764
935
|
*/
|
|
765
936
|
async clearInputBox(name) {
|
|
766
937
|
try {
|
|
938
|
+
let first = null;
|
|
767
939
|
for (let attempt = 0; attempt < 15; attempt += 1) {
|
|
768
940
|
// ダイアログ判定は selectionDialogVisible(末尾30行窓)に揃える。全画面を
|
|
769
941
|
// 走査すると、チャット本文に残る閉じたダイアログの転写(「Enter to select」行)を
|
|
@@ -777,12 +949,27 @@ export class HerdrSessionManager {
|
|
|
777
949
|
return false;
|
|
778
950
|
if (realText.replace(/\s+/g, "").length === 0)
|
|
779
951
|
return true;
|
|
952
|
+
first ??= realText;
|
|
780
953
|
await this.sendKeys(name, [KILL_LINE_SEQUENCE], true);
|
|
781
954
|
await new Promise((resolve) => setTimeout(resolve, this.clearKeyDelayMs));
|
|
782
955
|
}
|
|
783
956
|
// 上限到達時も最後の C-u の結果は未観測なので、最終状態を見てから判定する。
|
|
784
957
|
const realText = inputBoxRealText(await this.captureVisibleScreenAnsi(name));
|
|
785
|
-
|
|
958
|
+
if (realText === null)
|
|
959
|
+
return false;
|
|
960
|
+
if (realText.replace(/\s+/g, "").length === 0)
|
|
961
|
+
return true;
|
|
962
|
+
// 消えてはいるが行が多い(9 行以上の本文は 15 回では消し切れない)。進んでいる限り続ける。
|
|
963
|
+
// 1 字も消えていない残存(C-u の効かない中身)には、これ以上打たない。
|
|
964
|
+
// ダイアログが出ていたら `clearTypedInput` が 1 キーも打たない(フレームを確かめてから打つ)。
|
|
965
|
+
if (realText === first)
|
|
966
|
+
return false;
|
|
967
|
+
return await clearTypedInput(CLEAR_UNKNOWN_INPUT_BOUND, {
|
|
968
|
+
capture: () => this.captureVisibleScreenAnsiOrNull(name),
|
|
969
|
+
sendKills: (count) => this.sendKeys(name, [KILL_LINE_SEQUENCE.repeat(count)], true),
|
|
970
|
+
clearDelayMs: this.clearKeyDelayMs,
|
|
971
|
+
clearSettleMs: this.clearSettleMs,
|
|
972
|
+
});
|
|
786
973
|
}
|
|
787
974
|
catch {
|
|
788
975
|
return false;
|
|
@@ -903,7 +1090,7 @@ export class HerdrSessionManager {
|
|
|
903
1090
|
validateSessionName(name);
|
|
904
1091
|
const target = await this.paneTarget(name);
|
|
905
1092
|
if (target === null) {
|
|
906
|
-
throw
|
|
1093
|
+
throw await this.missingPaneError(["pane", "read", name]);
|
|
907
1094
|
}
|
|
908
1095
|
return this.readPane(target, ["--source", "visible", "--format", "ansi"]);
|
|
909
1096
|
}
|
|
@@ -931,7 +1118,7 @@ export class HerdrSessionManager {
|
|
|
931
1118
|
return;
|
|
932
1119
|
const target = await this.paneTarget(name);
|
|
933
1120
|
if (target === null) {
|
|
934
|
-
throw
|
|
1121
|
+
throw await this.missingPaneError(["pane", "send-keys", name]);
|
|
935
1122
|
}
|
|
936
1123
|
for (const key of keys) {
|
|
937
1124
|
let args;
|
|
@@ -966,7 +1153,7 @@ export class HerdrSessionManager {
|
|
|
966
1153
|
validateSessionName(name);
|
|
967
1154
|
const target = await this.paneTarget(name);
|
|
968
1155
|
if (target === null) {
|
|
969
|
-
throw
|
|
1156
|
+
throw await this.missingPaneError(["pane", "read", name]);
|
|
970
1157
|
}
|
|
971
1158
|
const lines = options.lines ?? this.captureLines;
|
|
972
1159
|
if (options.joinWrappedLines ?? false) {
|
|
@@ -991,7 +1178,7 @@ export class HerdrSessionManager {
|
|
|
991
1178
|
validateSessionName(name);
|
|
992
1179
|
const target = await this.paneTarget(name);
|
|
993
1180
|
if (target === null) {
|
|
994
|
-
throw
|
|
1181
|
+
throw await this.missingPaneError(["pane", "read", name]);
|
|
995
1182
|
}
|
|
996
1183
|
return this.readPane(target, ["--source", "visible"]);
|
|
997
1184
|
}
|
|
@@ -1026,6 +1213,24 @@ export class HerdrSessionManager {
|
|
|
1026
1213
|
// ENOENT 等(未導入環境)は無視。
|
|
1027
1214
|
}
|
|
1028
1215
|
}
|
|
1216
|
+
/**
|
|
1217
|
+
* 入出力先の pane が見つからなかったときのエラー。`pane list` 自体が旧サーバーとの非互換
|
|
1218
|
+
* (`protocol_mismatch`)で失敗していたなら、「pane が無い」ではなく更新の案内を返す
|
|
1219
|
+
* (livePanes は失敗を空集合として握るため、そのままだと送信失敗が `pane not found` に化ける)。
|
|
1220
|
+
* 見つからなかった後だけ 1 回追加で問い合わせるので、正常時の経路は増えない。
|
|
1221
|
+
*/
|
|
1222
|
+
async missingPaneError(args) {
|
|
1223
|
+
try {
|
|
1224
|
+
const result = await this.runner(["pane", "list"]);
|
|
1225
|
+
if (result.exitCode !== 0 && herdrErrorCode(result.stderr) === "protocol_mismatch") {
|
|
1226
|
+
return new HerdrFailedError(["pane", "list"], result.exitCode, result.stderr);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
catch {
|
|
1230
|
+
// 実行ファイル起動失敗などは従来どおり「pane が無い」として扱う。
|
|
1231
|
+
}
|
|
1232
|
+
return new HerdrFailedError(args, 1, "pane not found");
|
|
1233
|
+
}
|
|
1029
1234
|
/** 生存 pane の一覧。取得できなかった場合は null(「0 件」と区別する)。 */
|
|
1030
1235
|
async livePanesOrNull() {
|
|
1031
1236
|
try {
|
|
@@ -1044,14 +1249,11 @@ export class HerdrSessionManager {
|
|
|
1044
1249
|
}
|
|
1045
1250
|
/** セッション名の現存 pane を解決する(記録済み pane ID 優先、無ければ label 一致)。 */
|
|
1046
1251
|
async findPane(name) {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
return byId;
|
|
1053
|
-
}
|
|
1054
|
-
return panes.find((pane) => pane.label === name) ?? null;
|
|
1252
|
+
return resolveHerdrSessionPane(await this.livePanes(), name, this.store.get(name)?.herdrPaneId,
|
|
1253
|
+
// 3. の判定でだけ該当 1 件を読む(findPane はライブビュー更新で 250ms ごとに呼ばれるので
|
|
1254
|
+
// メタ全件は読まない)。名前の一致まで見るのは、大文字小文字を区別しない FS で
|
|
1255
|
+
// `S-B` が `s-b.json` を読んでも別名扱いにし、listFromPanes(中身の名前で判定)と揃えるため。
|
|
1256
|
+
(label) => this.store.get(label)?.name === label);
|
|
1055
1257
|
}
|
|
1056
1258
|
/** 入出力 target の pane ID(現存しなければ null)。 */
|
|
1057
1259
|
async paneTarget(name) {
|