tailii-host 0.1.1 → 0.3.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.
Files changed (348) hide show
  1. package/README.md +27 -10
  2. package/dist/{broker.d.ts → approval/broker.d.ts} +9 -5
  3. package/dist/{broker.js → approval/broker.js} +42 -6
  4. package/dist/approval/broker.js.map +1 -0
  5. package/dist/{hook.d.ts → approval/hook.d.ts} +48 -7
  6. package/dist/{hook.js → approval/hook.js} +359 -50
  7. package/dist/approval/hook.js.map +1 -0
  8. package/dist/backend/herdr.d.ts +226 -0
  9. package/dist/backend/herdr.js +749 -0
  10. package/dist/backend/herdr.js.map +1 -0
  11. package/dist/backend/sessionBackend.d.ts +81 -0
  12. package/dist/backend/sessionBackend.js +114 -0
  13. package/dist/backend/sessionBackend.js.map +1 -0
  14. package/dist/backend/tmux.d.ts +125 -0
  15. package/dist/backend/tmux.js +392 -0
  16. package/dist/backend/tmux.js.map +1 -0
  17. package/dist/{chatTailController.d.ts → chat/chatTailController.d.ts} +29 -6
  18. package/dist/{chatTailController.js → chat/chatTailController.js} +94 -18
  19. package/dist/chat/chatTailController.js.map +1 -0
  20. package/dist/{imageService.d.ts → chat/imageService.d.ts} +12 -1
  21. package/dist/{imageService.js → chat/imageService.js} +39 -11
  22. package/dist/chat/imageService.js.map +1 -0
  23. package/dist/chat/remoteQuestionMonitor.d.ts +27 -0
  24. package/dist/chat/remoteQuestionMonitor.js +128 -0
  25. package/dist/chat/remoteQuestionMonitor.js.map +1 -0
  26. package/dist/chat/subagentTailer.d.ts +30 -0
  27. package/dist/chat/subagentTailer.js +704 -0
  28. package/dist/chat/subagentTailer.js.map +1 -0
  29. package/dist/chat/subagentTranscript.d.ts +20 -0
  30. package/dist/chat/subagentTranscript.js +273 -0
  31. package/dist/chat/subagentTranscript.js.map +1 -0
  32. package/dist/{transcriptTailer.d.ts → chat/transcriptTailer.d.ts} +12 -1
  33. package/dist/{transcriptTailer.js → chat/transcriptTailer.js} +183 -10
  34. package/dist/chat/transcriptTailer.js.map +1 -0
  35. package/dist/cli.js +15 -10
  36. package/dist/cli.js.map +1 -1
  37. package/dist/codex/codexAccountUsage.d.ts +22 -0
  38. package/dist/codex/codexAccountUsage.js +128 -0
  39. package/dist/codex/codexAccountUsage.js.map +1 -0
  40. package/dist/codex/codexAppServer.d.ts +166 -0
  41. package/dist/codex/codexAppServer.js +1297 -0
  42. package/dist/codex/codexAppServer.js.map +1 -0
  43. package/dist/codex/codexNativeTurnController.d.ts +153 -0
  44. package/dist/codex/codexNativeTurnController.js +673 -0
  45. package/dist/codex/codexNativeTurnController.js.map +1 -0
  46. package/dist/codex/codexRolloutTailer.d.ts +100 -0
  47. package/dist/codex/codexRolloutTailer.js +568 -0
  48. package/dist/codex/codexRolloutTailer.js.map +1 -0
  49. package/dist/{codexSessionStore.d.ts → codex/codexSessionStore.d.ts} +10 -2
  50. package/dist/{codexSessionStore.js → codex/codexSessionStore.js} +184 -15
  51. package/dist/codex/codexSessionStore.js.map +1 -0
  52. package/dist/codex/codexSubagentTracker.d.ts +21 -0
  53. package/dist/codex/codexSubagentTracker.js +281 -0
  54. package/dist/codex/codexSubagentTracker.js.map +1 -0
  55. package/dist/codex/codexToolActivity.d.ts +69 -0
  56. package/dist/codex/codexToolActivity.js +425 -0
  57. package/dist/codex/codexToolActivity.js.map +1 -0
  58. package/dist/{codexUsage.d.ts → codex/codexUsage.d.ts} +1 -1
  59. package/dist/{codexUsage.js → codex/codexUsage.js} +19 -26
  60. package/dist/codex/codexUsage.js.map +1 -0
  61. package/dist/commands/doctor.d.ts +65 -0
  62. package/dist/commands/doctor.js +425 -0
  63. package/dist/commands/doctor.js.map +1 -0
  64. package/dist/{hookSettings.d.ts → commands/hookSettings.d.ts} +14 -1
  65. package/dist/{hookSettings.js → commands/hookSettings.js} +96 -12
  66. package/dist/commands/hookSettings.js.map +1 -0
  67. package/dist/{kick.js → commands/kick.js} +16 -1
  68. package/dist/commands/kick.js.map +1 -0
  69. package/dist/{launch.d.ts → commands/launch.d.ts} +67 -5
  70. package/dist/commands/launch.js +716 -0
  71. package/dist/commands/launch.js.map +1 -0
  72. package/dist/commands/legacyHomeMigration.js.map +1 -0
  73. package/dist/{pairingCode.d.ts → commands/pairingCode.d.ts} +29 -2
  74. package/dist/{pairingCode.js → commands/pairingCode.js} +120 -11
  75. package/dist/commands/pairingCode.js.map +1 -0
  76. package/dist/{pushTokenCommand.d.ts → commands/pushTokenCommand.d.ts} +1 -1
  77. package/dist/{pushTokenCommand.js → commands/pushTokenCommand.js} +2 -2
  78. package/dist/commands/pushTokenCommand.js.map +1 -0
  79. package/dist/{setup.d.ts → commands/setup.d.ts} +17 -6
  80. package/dist/{setup.js → commands/setup.js} +133 -75
  81. package/dist/commands/setup.js.map +1 -0
  82. package/dist/engine/context.d.ts +145 -0
  83. package/dist/engine/context.js +184 -0
  84. package/dist/engine/context.js.map +1 -0
  85. package/dist/engine/engine.d.ts +121 -0
  86. package/dist/engine/engine.js +799 -0
  87. package/dist/engine/engine.js.map +1 -0
  88. package/dist/engine/handlers/accountUsage.d.ts +53 -0
  89. package/dist/engine/handlers/accountUsage.js +203 -0
  90. package/dist/engine/handlers/accountUsage.js.map +1 -0
  91. package/dist/engine/handlers/claudeModels.d.ts +2 -0
  92. package/dist/engine/handlers/claudeModels.js +23 -0
  93. package/dist/engine/handlers/claudeModels.js.map +1 -0
  94. package/dist/engine/handlers/codex.d.ts +2 -0
  95. package/dist/engine/handlers/codex.js +97 -0
  96. package/dist/engine/handlers/codex.js.map +1 -0
  97. package/dist/engine/handlers/conversation.d.ts +2 -0
  98. package/dist/engine/handlers/conversation.js +259 -0
  99. package/dist/engine/handlers/conversation.js.map +1 -0
  100. package/dist/engine/handlers/core.d.ts +2 -0
  101. package/dist/engine/handlers/core.js +66 -0
  102. package/dist/engine/handlers/core.js.map +1 -0
  103. package/dist/engine/handlers/git.d.ts +2 -0
  104. package/dist/engine/handlers/git.js +127 -0
  105. package/dist/engine/handlers/git.js.map +1 -0
  106. package/dist/engine/handlers/index.d.ts +2 -0
  107. package/dist/engine/handlers/index.js +32 -0
  108. package/dist/engine/handlers/index.js.map +1 -0
  109. package/dist/engine/handlers/mode.d.ts +2 -0
  110. package/dist/engine/handlers/mode.js +194 -0
  111. package/dist/engine/handlers/mode.js.map +1 -0
  112. package/dist/engine/handlers/officialApps.d.ts +2 -0
  113. package/dist/engine/handlers/officialApps.js +79 -0
  114. package/dist/engine/handlers/officialApps.js.map +1 -0
  115. package/dist/engine/handlers/preview.d.ts +2 -0
  116. package/dist/engine/handlers/preview.js +59 -0
  117. package/dist/engine/handlers/preview.js.map +1 -0
  118. package/dist/engine/handlers/session.d.ts +2 -0
  119. package/dist/engine/handlers/session.js +385 -0
  120. package/dist/engine/handlers/session.js.map +1 -0
  121. package/dist/engine/handlers/usage.d.ts +2 -0
  122. package/dist/engine/handlers/usage.js +96 -0
  123. package/dist/engine/handlers/usage.js.map +1 -0
  124. package/dist/engine/handlers/workspace.d.ts +2 -0
  125. package/dist/engine/handlers/workspace.js +110 -0
  126. package/dist/engine/handlers/workspace.js.map +1 -0
  127. package/dist/engine/slashCommands.d.ts +3 -0
  128. package/dist/engine/slashCommands.js +211 -0
  129. package/dist/engine/slashCommands.js.map +1 -0
  130. package/dist/hub/engineRelaySocket.d.ts +58 -0
  131. package/dist/hub/engineRelaySocket.js +161 -0
  132. package/dist/hub/engineRelaySocket.js.map +1 -0
  133. package/dist/hub/hubClient.d.ts +28 -0
  134. package/dist/hub/hubClient.js +187 -0
  135. package/dist/hub/hubClient.js.map +1 -0
  136. package/dist/hub/hubDaemon.d.ts +69 -0
  137. package/dist/hub/hubDaemon.js +466 -0
  138. package/dist/hub/hubDaemon.js.map +1 -0
  139. package/dist/hub/hubProtocol.d.ts +161 -0
  140. package/dist/hub/hubProtocol.js +379 -0
  141. package/dist/hub/hubProtocol.js.map +1 -0
  142. package/dist/hub/panePreviewPump.d.ts +65 -0
  143. package/dist/hub/panePreviewPump.js +187 -0
  144. package/dist/hub/panePreviewPump.js.map +1 -0
  145. package/dist/hub/questionInjection.d.ts +19 -0
  146. package/dist/hub/questionInjection.js +126 -0
  147. package/dist/hub/questionInjection.js.map +1 -0
  148. package/dist/hub/reaper.d.ts +74 -0
  149. package/dist/hub/reaper.js +333 -0
  150. package/dist/hub/reaper.js.map +1 -0
  151. package/dist/hub/sessionHub.d.ts +203 -0
  152. package/dist/hub/sessionHub.js +1862 -0
  153. package/dist/hub/sessionHub.js.map +1 -0
  154. package/dist/protocol/common.d.ts +18 -0
  155. package/dist/protocol/common.js +78 -0
  156. package/dist/protocol/common.js.map +1 -0
  157. package/dist/protocol/decode.d.ts +3 -0
  158. package/dist/protocol/decode.js +1036 -0
  159. package/dist/protocol/decode.js.map +1 -0
  160. package/dist/protocol/encode.d.ts +3 -0
  161. package/dist/protocol/encode.js +84 -0
  162. package/dist/protocol/encode.js.map +1 -0
  163. package/dist/protocol/messages.d.ts +1002 -0
  164. package/dist/protocol/messages.js +8 -0
  165. package/dist/protocol/messages.js.map +1 -0
  166. package/dist/protocol.d.ts +4 -329
  167. package/dist/protocol.js +13 -531
  168. package/dist/protocol.js.map +1 -1
  169. package/dist/push/apnsConfigStore.js.map +1 -0
  170. package/dist/push/apnsJwtProvider.js.map +1 -0
  171. package/dist/{apnsSender.js.map → push/apnsSender.js.map} +1 -1
  172. package/dist/{approvalPushNotifier.js → push/approvalPushNotifier.js} +1 -1
  173. package/dist/push/approvalPushNotifier.js.map +1 -0
  174. package/dist/push/deviceTokenStore.js.map +1 -0
  175. package/dist/{pushTypes.js → push/pushTypes.js} +1 -1
  176. package/dist/push/pushTypes.js.map +1 -0
  177. package/dist/push/sendLog.js.map +1 -0
  178. package/dist/services/accountIdentity.d.ts +44 -0
  179. package/dist/services/accountIdentity.js +184 -0
  180. package/dist/services/accountIdentity.js.map +1 -0
  181. package/dist/services/claudeCredentialRefresh.d.ts +30 -0
  182. package/dist/services/claudeCredentialRefresh.js +345 -0
  183. package/dist/services/claudeCredentialRefresh.js.map +1 -0
  184. package/dist/services/claudeModelCatalog.d.ts +21 -0
  185. package/dist/services/claudeModelCatalog.js +181 -0
  186. package/dist/services/claudeModelCatalog.js.map +1 -0
  187. package/dist/{dirLister.d.ts → services/dirLister.d.ts} +9 -4
  188. package/dist/services/dirLister.js +169 -0
  189. package/dist/services/dirLister.js.map +1 -0
  190. package/dist/services/fileService.d.ts +10 -0
  191. package/dist/services/fileService.js +123 -0
  192. package/dist/services/fileService.js.map +1 -0
  193. package/dist/services/gitService.d.ts +79 -0
  194. package/dist/services/gitService.js +648 -0
  195. package/dist/services/gitService.js.map +1 -0
  196. package/dist/services/hostVersions.d.ts +38 -0
  197. package/dist/services/hostVersions.js +120 -0
  198. package/dist/services/hostVersions.js.map +1 -0
  199. package/dist/services/officialApps.d.ts +148 -0
  200. package/dist/services/officialApps.js +1079 -0
  201. package/dist/services/officialApps.js.map +1 -0
  202. package/dist/services/planUsageFetcher.d.ts +119 -0
  203. package/dist/services/planUsageFetcher.js +357 -0
  204. package/dist/services/planUsageFetcher.js.map +1 -0
  205. package/dist/services/previewServer.d.ts +31 -0
  206. package/dist/services/previewServer.js +233 -0
  207. package/dist/services/previewServer.js.map +1 -0
  208. package/dist/services/quicGateway.d.ts +143 -0
  209. package/dist/services/quicGateway.js +292 -0
  210. package/dist/services/quicGateway.js.map +1 -0
  211. package/dist/services/serveService.d.ts +50 -0
  212. package/dist/services/serveService.js +302 -0
  213. package/dist/services/serveService.js.map +1 -0
  214. package/dist/services/usageAggregator.js.map +1 -0
  215. package/dist/sessions/claudeSessionStore.d.ts +80 -0
  216. package/dist/sessions/claudeSessionStore.js +477 -0
  217. package/dist/sessions/claudeSessionStore.js.map +1 -0
  218. package/dist/sessions/heartbeat.d.ts +24 -0
  219. package/dist/sessions/heartbeat.js +86 -0
  220. package/dist/sessions/heartbeat.js.map +1 -0
  221. package/dist/sessions/liveSessionJoin.d.ts +20 -0
  222. package/dist/sessions/liveSessionJoin.js +80 -0
  223. package/dist/sessions/liveSessionJoin.js.map +1 -0
  224. package/dist/sessions/sessionActivityProvider.d.ts +14 -0
  225. package/dist/sessions/sessionActivityProvider.js +57 -0
  226. package/dist/sessions/sessionActivityProvider.js.map +1 -0
  227. package/dist/{sessionListService.d.ts → sessions/sessionListService.d.ts} +3 -3
  228. package/dist/{sessionListService.js → sessions/sessionListService.js} +23 -6
  229. package/dist/sessions/sessionListService.js.map +1 -0
  230. package/dist/{sessionMetadataStore.d.ts → sessions/sessionMetadataStore.d.ts} +18 -0
  231. package/dist/{sessionMetadataStore.js → sessions/sessionMetadataStore.js} +36 -1
  232. package/dist/sessions/sessionMetadataStore.js.map +1 -0
  233. package/dist/{sessionSearch.d.ts → sessions/sessionSearch.d.ts} +1 -1
  234. package/dist/sessions/sessionSearch.js.map +1 -0
  235. package/dist/shared/hostDisplayName.d.ts +11 -0
  236. package/dist/shared/hostDisplayName.js +50 -0
  237. package/dist/shared/hostDisplayName.js.map +1 -0
  238. package/dist/{lineWriter.d.ts → shared/lineWriter.d.ts} +1 -1
  239. package/dist/{lineWriter.js → shared/lineWriter.js} +1 -1
  240. package/dist/shared/lineWriter.js.map +1 -0
  241. package/dist/{observationLog.js.map → shared/observationLog.js.map} +1 -1
  242. package/dist/{paths.d.ts → shared/paths.d.ts} +7 -0
  243. package/dist/{paths.js → shared/paths.js} +9 -0
  244. package/dist/shared/paths.js.map +1 -0
  245. package/dist/shared/permissionMode.d.ts +10 -0
  246. package/dist/shared/permissionMode.js +51 -0
  247. package/dist/shared/permissionMode.js.map +1 -0
  248. package/dist/shared/skillInjection.d.ts +9 -0
  249. package/dist/shared/skillInjection.js +16 -0
  250. package/dist/shared/skillInjection.js.map +1 -0
  251. package/dist/shared/sleep.js.map +1 -0
  252. package/dist/{socketPath.d.ts → shared/socketPath.d.ts} +4 -0
  253. package/dist/{socketPath.js → shared/socketPath.js} +22 -0
  254. package/dist/shared/socketPath.js.map +1 -0
  255. package/dist/shared/version.js.map +1 -0
  256. package/package.json +13 -7
  257. package/dist/apnsConfigStore.js.map +0 -1
  258. package/dist/apnsJwtProvider.js.map +0 -1
  259. package/dist/approvalPushNotifier.js.map +0 -1
  260. package/dist/broker.js.map +0 -1
  261. package/dist/chatTailController.js.map +0 -1
  262. package/dist/claudeSessionStore.d.ts +0 -15
  263. package/dist/claudeSessionStore.js +0 -201
  264. package/dist/claudeSessionStore.js.map +0 -1
  265. package/dist/codexRolloutTailer.d.ts +0 -56
  266. package/dist/codexRolloutTailer.js +0 -303
  267. package/dist/codexRolloutTailer.js.map +0 -1
  268. package/dist/codexSessionStore.js.map +0 -1
  269. package/dist/codexUsage.js.map +0 -1
  270. package/dist/deviceTokenStore.js.map +0 -1
  271. package/dist/dirLister.js +0 -98
  272. package/dist/dirLister.js.map +0 -1
  273. package/dist/doctor.d.ts +0 -36
  274. package/dist/doctor.js +0 -162
  275. package/dist/doctor.js.map +0 -1
  276. package/dist/engine.d.ts +0 -85
  277. package/dist/engine.js +0 -962
  278. package/dist/engine.js.map +0 -1
  279. package/dist/hook.js.map +0 -1
  280. package/dist/hookSettings.js.map +0 -1
  281. package/dist/idleReaper.d.ts +0 -13
  282. package/dist/idleReaper.js +0 -30
  283. package/dist/idleReaper.js.map +0 -1
  284. package/dist/imageService.js.map +0 -1
  285. package/dist/kick.js.map +0 -1
  286. package/dist/launch.js +0 -396
  287. package/dist/launch.js.map +0 -1
  288. package/dist/legacyHomeMigration.js.map +0 -1
  289. package/dist/lineWriter.js.map +0 -1
  290. package/dist/mdnsAdvertiser.d.ts +0 -46
  291. package/dist/mdnsAdvertiser.js +0 -268
  292. package/dist/mdnsAdvertiser.js.map +0 -1
  293. package/dist/pairingCode.js.map +0 -1
  294. package/dist/paths.js.map +0 -1
  295. package/dist/permissionMode.d.ts +0 -8
  296. package/dist/permissionMode.js +0 -32
  297. package/dist/permissionMode.js.map +0 -1
  298. package/dist/planUsageFetcher.d.ts +0 -35
  299. package/dist/planUsageFetcher.js +0 -172
  300. package/dist/planUsageFetcher.js.map +0 -1
  301. package/dist/pushTokenCommand.js.map +0 -1
  302. package/dist/pushTypes.js.map +0 -1
  303. package/dist/sendLog.js.map +0 -1
  304. package/dist/sessionActivityProvider.d.ts +0 -7
  305. package/dist/sessionActivityProvider.js +0 -44
  306. package/dist/sessionActivityProvider.js.map +0 -1
  307. package/dist/sessionIdleTracker.d.ts +0 -19
  308. package/dist/sessionIdleTracker.js +0 -42
  309. package/dist/sessionIdleTracker.js.map +0 -1
  310. package/dist/sessionListService.js.map +0 -1
  311. package/dist/sessionMetadataStore.js.map +0 -1
  312. package/dist/sessionSearch.js.map +0 -1
  313. package/dist/setup.js.map +0 -1
  314. package/dist/sleep.js.map +0 -1
  315. package/dist/socketPath.js.map +0 -1
  316. package/dist/subagentTailer.d.ts +0 -15
  317. package/dist/subagentTailer.js +0 -399
  318. package/dist/subagentTailer.js.map +0 -1
  319. package/dist/tmux.d.ts +0 -62
  320. package/dist/tmux.js +0 -163
  321. package/dist/tmux.js.map +0 -1
  322. package/dist/transcriptTailer.js.map +0 -1
  323. package/dist/usageAggregator.js.map +0 -1
  324. package/dist/version.js.map +0 -1
  325. /package/dist/{kick.d.ts → commands/kick.d.ts} +0 -0
  326. /package/dist/{legacyHomeMigration.d.ts → commands/legacyHomeMigration.d.ts} +0 -0
  327. /package/dist/{legacyHomeMigration.js → commands/legacyHomeMigration.js} +0 -0
  328. /package/dist/{apnsConfigStore.d.ts → push/apnsConfigStore.d.ts} +0 -0
  329. /package/dist/{apnsConfigStore.js → push/apnsConfigStore.js} +0 -0
  330. /package/dist/{apnsJwtProvider.d.ts → push/apnsJwtProvider.d.ts} +0 -0
  331. /package/dist/{apnsJwtProvider.js → push/apnsJwtProvider.js} +0 -0
  332. /package/dist/{apnsSender.d.ts → push/apnsSender.d.ts} +0 -0
  333. /package/dist/{apnsSender.js → push/apnsSender.js} +0 -0
  334. /package/dist/{approvalPushNotifier.d.ts → push/approvalPushNotifier.d.ts} +0 -0
  335. /package/dist/{deviceTokenStore.d.ts → push/deviceTokenStore.d.ts} +0 -0
  336. /package/dist/{deviceTokenStore.js → push/deviceTokenStore.js} +0 -0
  337. /package/dist/{pushTypes.d.ts → push/pushTypes.d.ts} +0 -0
  338. /package/dist/{sendLog.d.ts → push/sendLog.d.ts} +0 -0
  339. /package/dist/{sendLog.js → push/sendLog.js} +0 -0
  340. /package/dist/{usageAggregator.d.ts → services/usageAggregator.d.ts} +0 -0
  341. /package/dist/{usageAggregator.js → services/usageAggregator.js} +0 -0
  342. /package/dist/{sessionSearch.js → sessions/sessionSearch.js} +0 -0
  343. /package/dist/{observationLog.d.ts → shared/observationLog.d.ts} +0 -0
  344. /package/dist/{observationLog.js → shared/observationLog.js} +0 -0
  345. /package/dist/{sleep.d.ts → shared/sleep.d.ts} +0 -0
  346. /package/dist/{sleep.js → shared/sleep.js} +0 -0
  347. /package/dist/{version.d.ts → shared/version.d.ts} +0 -0
  348. /package/dist/{version.js → shared/version.js} +0 -0
@@ -0,0 +1,749 @@
1
+ // herdr.ts
2
+ // tailii (TS host) — herdr backend のセッション操作(list / reattach / kill / send / read)。
3
+ // TmuxSessionManager と同じ面(SessionBackend)を herdr CLI(socket API のフロント)で実装する。
4
+ //
5
+ // 実測済みの herdr 0.7.5 挙動(設計正本は docs/herdr-backend.md):
6
+ // - 全 pane は専用 named session `tailii` に収容(全コマンドに `--session tailii` 前置)。
7
+ // 起動は `tab create --cwd --env --no-focus`(root_pane.pane_id が新 pane)→ `pane rename`
8
+ // → `pane run <id> "exec /bin/zsh -lc '<cmd>'"`。0.7.5 で `agent start` から pane 生成
9
+ // /cwd/env 指定が廃止されたため(旧: agent start 一発で pane 起動)。
10
+ // `pane run` はタイプ注入で argv のクォートを保持しない → コマンドは単一文字列で渡す。
11
+ // exec 前置によりコマンド終了と同時に pane が閉じる(旧 agent start と同義・stale shell なし)。
12
+ // - `pane read --source recent-unwrapped --lines N` は tmux `capture-pane -J -S -N` 相当。
13
+ // ただし出力リングが空の新規 pane では空文字を返すため visible へフォールバックする。
14
+ // `--source visible` は viewport 全行を返し `--lines` を無視する(自前で末尾を切る)。
15
+ // - `pane send-keys` は Escape/Up/Down/Left/Right/Tab/Space/C-x 系のみ実用。
16
+ // Enter は claude TUI(Ink) が submit と認識しないため生 CR を `send-text` で送る。
17
+ // BTab(Shift+Tab)は不可 → 生シーケンス ESC [ Z を `pane send-text` で送る(cat -v 検証済み)。
18
+ // - `pane process-info` の foreground_processes[0].name が tmux `#{pane_current_command}` 相当。
19
+ import { execFile } from "node:child_process";
20
+ import * as fs from "node:fs";
21
+ import * as os from "node:os";
22
+ import * as path from "node:path";
23
+ import { PROTOCOL_V1 } from "../protocol.js";
24
+ import { HERDR_PANE_ID_PATTERN, SessionMetadataStore, validateSessionName, } from "../sessions/sessionMetadataStore.js";
25
+ import { extractClaudeInputBox, inputBoxIsShellMode, paneCommandLooksLikeAgent, } from "./tmux.js";
26
+ /** herdr 実行ファイルの既定絶対パス(SSH exec は非ログインシェルで PATH 外のため絶対指定)。 */
27
+ export function defaultHerdrPath() {
28
+ return path.join(os.homedir(), ".local", "bin", "herdr");
29
+ }
30
+ /**
31
+ * Tailii セッション pane を収容する herdr named session。
32
+ * ユーザーの default セッション(普段使いの herdr)を汚さないよう、Tailii は専用の
33
+ * named session(別サーバー・別ソケット `~/.config/herdr/sessions/tailii/`)に全 pane を
34
+ * 収容する。CLI は全操作に `--session tailii` を前置する(runner レベルで一元付与)。
35
+ * まとめて消すときは `herdr session stop tailii` → `herdr session delete tailii`。
36
+ */
37
+ export const HERDR_TAILII_SESSION = "tailii";
38
+ /** herdr が導入済みか(backend_get の可用性表示・backend_set の検証に使う)。 */
39
+ export function herdrInstalled(herdrPath = defaultHerdrPath()) {
40
+ try {
41
+ fs.accessSync(herdrPath, fs.constants.X_OK);
42
+ return true;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ /**
49
+ * 実 herdr を起動する既定ランナー。herdr 非0 exit は throw せず結果で表現する。
50
+ * `sessionName`(既定 tailii)を `--session` として全コマンドに前置する。
51
+ */
52
+ export function processHerdrCommandRunner(herdrPath = defaultHerdrPath(), sessionName = HERDR_TAILII_SESSION) {
53
+ return (args) => new Promise((resolve, reject) => {
54
+ execFile(herdrPath, ["--session", sessionName, ...args], { maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
55
+ if (error && typeof error.code === "string") {
56
+ // 実行ファイル起動自体の失敗(ENOENT 等)のみ throw(tmux ランナーと同じ境界)。
57
+ reject(error);
58
+ return;
59
+ }
60
+ const exitCode = error && typeof error.code === "number" ? error.code : error ? 1 : 0;
61
+ resolve({ exitCode, stdout: String(stdout), stderr: String(stderr) });
62
+ });
63
+ });
64
+ }
65
+ /** HerdrSessionManager が投げる型付きエラー。 */
66
+ export class HerdrFailedError extends Error {
67
+ args;
68
+ exitCode;
69
+ stderr;
70
+ constructor(args, exitCode, stderr) {
71
+ super(`herdr ${args.join(" ")} failed (exit ${exitCode}): ${stderr}`);
72
+ this.args = args;
73
+ this.exitCode = exitCode;
74
+ this.stderr = stderr;
75
+ this.name = "HerdrFailedError";
76
+ }
77
+ }
78
+ /** `pane send-keys` が受理する tmux 互換キー名(実測)。それ以外はテキスト送出へ写像する。 */
79
+ const HERDR_KEY_NAMES = new Set([
80
+ "Enter",
81
+ "Escape",
82
+ "Up",
83
+ "Down",
84
+ "Left",
85
+ "Right",
86
+ "Tab",
87
+ "Space",
88
+ // シェルモード離脱(空入力の `!` を消す)に使う。テキスト送出へ落ちると
89
+ // 「Backspace」の 9 文字がそのまま入力欄へ打ち込まれるので必ずキー名で送る。
90
+ "Backspace",
91
+ ]);
92
+ /** Shift+Tab の生シーケンス(herdr send-keys は BTab を受理しないため send-text で送る)。 */
93
+ const SHIFT_TAB_SEQUENCE = "\u001b[Z";
94
+ /**
95
+ * Enter の生シーケンス(CR)。herdr の `send-keys Enter` は zsh 等の行編集では確定として
96
+ * 効くが、claude TUI(Ink) は submit と認識しない(実測 2026-07-22: 注入本文が入力欄に
97
+ * 残ったまま新規会話が始まらない)。Ink の return 判定は CR そのものを要求するため
98
+ * `send-text` で生 `\r` を送る。
99
+ */
100
+ const ENTER_SEQUENCE = "\r";
101
+ /**
102
+ * 画面に選択ダイアログのフッター行があるか。チャット本文が「Enter to select」を
103
+ * 引用しただけの偽陽性(誤 Esc・再送スキップの実害)を避けるため、substring では
104
+ * なく trim 後にフッター文言で始まる行だけをダイアログとみなす。
105
+ */
106
+ export function screenHasSelectionFooter(screen) {
107
+ return screen
108
+ .split("\n")
109
+ .some((line) => line.trim().startsWith("Enter to select"));
110
+ }
111
+ /**
112
+ * 入力反映検証に使う probe(本文 1 行目の先頭部分)。検証不能な本文(空など)は null。
113
+ *
114
+ * 先頭 `!`(シェルモード)は claude TUI がモード記号として吸い上げ、入力欄本文には
115
+ * 残らない(`!ls -la` は `! ls -la` と描画される)。照合キーからも落とさないと
116
+ * 反映検証が必ず失敗する。
117
+ */
118
+ export function typedTextProbe(text) {
119
+ const firstLine = (text.split("\n")[0] ?? "").trim();
120
+ const body = firstLine.startsWith("!") ? firstLine.slice(1).trim() : firstLine;
121
+ if (body.length === 0)
122
+ return null;
123
+ return body.slice(0, 24);
124
+ }
125
+ /** herdr CLI の JSON stdout から `result` を取り出す。JSON でない/エラー封筒は null。 */
126
+ export function parseHerdrResult(stdout) {
127
+ try {
128
+ const parsed = JSON.parse(stdout);
129
+ if (typeof parsed !== "object" || parsed === null)
130
+ return null;
131
+ const result = parsed["result"];
132
+ if (typeof result !== "object" || result === null)
133
+ return null;
134
+ return result;
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ /** `pane list` stdout をパースする(形式不明は空配列)。 */
141
+ export function parseHerdrPaneList(stdout) {
142
+ const result = parseHerdrResult(stdout);
143
+ const panes = result?.["panes"];
144
+ if (!Array.isArray(panes))
145
+ return [];
146
+ const out = [];
147
+ for (const raw of panes) {
148
+ if (typeof raw !== "object" || raw === null)
149
+ continue;
150
+ const obj = raw;
151
+ if (typeof obj["pane_id"] !== "string")
152
+ continue;
153
+ out.push({
154
+ paneId: obj["pane_id"],
155
+ label: typeof obj["label"] === "string" ? obj["label"] : null,
156
+ tabId: typeof obj["tab_id"] === "string" ? obj["tab_id"] : null,
157
+ });
158
+ }
159
+ return out;
160
+ }
161
+ /**
162
+ * herdr 0.7.5 の `tab create` が自動付与する既定タブラベル(連番文字列 "1"/"2"…)か。
163
+ * 既定ラベルは「未命名」として扱う: hub tick の自動タイトルは上書きしてよく、
164
+ * 逆方向同期(displayTitle)は人為リネームと誤認して会話タイトルへ取り込んではならない。
165
+ */
166
+ export function isDefaultHerdrTabLabel(label) {
167
+ return /^\d+$/.test(label);
168
+ }
169
+ /** `tab list` stdout を tab_id → label のマップにパースする(形式不明は空マップ)。 */
170
+ export function parseHerdrTabLabels(stdout) {
171
+ const result = parseHerdrResult(stdout);
172
+ const tabs = result?.["tabs"];
173
+ const out = new Map();
174
+ if (!Array.isArray(tabs))
175
+ return out;
176
+ for (const raw of tabs) {
177
+ if (typeof raw !== "object" || raw === null)
178
+ continue;
179
+ const obj = raw;
180
+ if (typeof obj["tab_id"] === "string" && typeof obj["label"] === "string") {
181
+ out.set(obj["tab_id"], obj["label"]);
182
+ }
183
+ }
184
+ return out;
185
+ }
186
+ /** `workspace list` から label 一致の workspace_id を探す(不在は null)。 */
187
+ export function parseHerdrWorkspaceId(stdout, label) {
188
+ const result = parseHerdrResult(stdout);
189
+ const workspaces = result?.["workspaces"];
190
+ if (!Array.isArray(workspaces))
191
+ return null;
192
+ for (const raw of workspaces) {
193
+ if (typeof raw !== "object" || raw === null)
194
+ continue;
195
+ const obj = raw;
196
+ if (obj["label"] === label && typeof obj["workspace_id"] === "string") {
197
+ return obj["workspace_id"];
198
+ }
199
+ }
200
+ return null;
201
+ }
202
+ /** `workspace create` stdout から workspace_id を取り出す(失敗は null)。 */
203
+ export function parseHerdrCreatedWorkspaceId(stdout) {
204
+ const result = parseHerdrResult(stdout);
205
+ const workspace = result?.["workspace"];
206
+ if (typeof workspace !== "object" || workspace === null)
207
+ return null;
208
+ const id = workspace["workspace_id"];
209
+ return typeof id === "string" ? id : null;
210
+ }
211
+ /** `agent start` stdout から pane_id を取り出す(失敗は null)。 */
212
+ export function parseHerdrStartedPaneId(stdout) {
213
+ const result = parseHerdrResult(stdout);
214
+ const agent = result?.["agent"];
215
+ if (typeof agent !== "object" || agent === null)
216
+ return null;
217
+ const id = agent["pane_id"];
218
+ return typeof id === "string" && HERDR_PANE_ID_PATTERN.test(id) ? id : null;
219
+ }
220
+ /** `tab create` stdout から root pane の pane_id を取り出す(失敗は null)。 */
221
+ export function parseHerdrCreatedTabPaneId(stdout) {
222
+ const result = parseHerdrResult(stdout);
223
+ const rootPane = result?.["root_pane"];
224
+ if (typeof rootPane !== "object" || rootPane === null)
225
+ return null;
226
+ const id = rootPane["pane_id"];
227
+ return typeof id === "string" && HERDR_PANE_ID_PATTERN.test(id) ? id : null;
228
+ }
229
+ /**
230
+ * `pane get` stdout から agent_status を取り出す(判定不能は null)。
231
+ * 0.7.5 では `agent get <name>` が使えない(agent レジストリは herdr の自動検出のみで
232
+ * Tailii のセッション名を知らず agent_not_found になる)ため、pane 単位の status を読む。
233
+ */
234
+ export function parseHerdrPaneAgentStatus(stdout) {
235
+ const result = parseHerdrResult(stdout);
236
+ const pane = result?.["pane"];
237
+ if (typeof pane !== "object" || pane === null)
238
+ return null;
239
+ const status = pane["agent_status"];
240
+ return typeof status === "string" ? status : null;
241
+ }
242
+ /** `pane process-info` stdout から前面プロセス名を取り出す(判定不能は空文字)。 */
243
+ export function parseHerdrForegroundCommand(stdout) {
244
+ const result = parseHerdrResult(stdout);
245
+ const info = result?.["process_info"];
246
+ if (typeof info !== "object" || info === null)
247
+ return "";
248
+ const list = info["foreground_processes"];
249
+ if (!Array.isArray(list) || list.length === 0)
250
+ return "";
251
+ const first = list[0];
252
+ if (typeof first !== "object" || first === null)
253
+ return "";
254
+ const name = first["name"];
255
+ return typeof name === "string" ? name : "";
256
+ }
257
+ /** herdr backend のセッション list / reattach / kill / send / read とメタデータ統合。 */
258
+ export class HerdrSessionManager {
259
+ runner;
260
+ store;
261
+ captureLines;
262
+ protocolVersion;
263
+ /** sendTextSubmit の本文→CR 間隔 ms(実測 300ms 未満で CR が飲まれる。テスト注入用)。 */
264
+ submitDelayMs;
265
+ /** sendTextSubmit の CR→残留確認 間隔 ms(テスト注入用)。 */
266
+ submitVerifyDelayMs;
267
+ /** 入力欄へ本文が反映されなかったときの再投入間隔 ms(RC 切断 limbo 対策)。 */
268
+ inputRetryDelayMs;
269
+ /** 注入前の claude 検出待ちの上限/間隔 ms(テスト注入用)。 */
270
+ readyTimeoutMs;
271
+ readyPollMs;
272
+ constructor(options = {}) {
273
+ this.runner = options.runner ?? processHerdrCommandRunner();
274
+ this.store = options.store ?? new SessionMetadataStore();
275
+ this.captureLines = options.captureLines ?? 50;
276
+ this.protocolVersion = options.protocolVersion ?? PROTOCOL_V1;
277
+ this.submitDelayMs = options.submitDelayMs ?? 600;
278
+ this.submitVerifyDelayMs = options.submitVerifyDelayMs ?? 700;
279
+ this.inputRetryDelayMs = options.inputRetryDelayMs ?? 1_500;
280
+ this.readyTimeoutMs = options.readyTimeoutMs ?? 10_000;
281
+ this.readyPollMs = options.readyPollMs ?? 300;
282
+ }
283
+ /**
284
+ * herdr 担当(メタの backend=herdr)のセッションを name/cwd/alive で列挙する(name 昇順)。
285
+ * tmux 側と異なり herdr pane は名前で自己申告しないため、メタデータが列挙の権威。
286
+ * 生存は「記録済み pane ID が現存」または「pane label が session 名に一致」。
287
+ */
288
+ async list() {
289
+ // herdr メタが皆無なら CLI を呼ばない(常時 Composite 構成でも純 tmux 環境に副作用ゼロ)。
290
+ if (!this.store.all().some((meta) => meta.backend === "herdr"))
291
+ return [];
292
+ const panes = await this.livePanes();
293
+ const paneIds = new Set(panes.map((pane) => pane.paneId));
294
+ const labels = new Set(panes.map((pane) => pane.label).filter((label) => label !== null));
295
+ // タブラベル(Mac 側リネーム含む会話タイトル表示)を name→label で引けるようにする
296
+ // (session-title 逆方向同期: セッション名と異なるラベルを displayTitle として載せる)。
297
+ const tabLabels = await this.tabLabels();
298
+ const infos = [];
299
+ for (const meta of this.store.all()) {
300
+ if (meta.backend !== "herdr")
301
+ continue;
302
+ const agent = meta.agent ?? "claude";
303
+ const providerSessionId = meta.providerSessionId ?? (agent === "claude" ? meta.claudeSessionId : undefined);
304
+ const alive = (meta.herdrPaneId !== undefined && paneIds.has(meta.herdrPaneId)) || labels.has(meta.name);
305
+ const pane = (meta.herdrPaneId !== undefined
306
+ ? panes.find((candidate) => candidate.paneId === meta.herdrPaneId)
307
+ : undefined) ?? panes.find((candidate) => candidate.label === meta.name);
308
+ const tabLabel = pane?.tabId != null ? tabLabels.get(pane.tabId) : undefined;
309
+ // 自動適用ラベル(hub tick が書いた値 = meta.autoTabTitle)と既定連番は人為リネーム
310
+ // ではないため displayTitle に載せない(iOS が override として誤取り込みしないため)。
311
+ const displayTitle = alive && tabLabel !== undefined && tabLabel !== meta.name && tabLabel.length > 0 &&
312
+ !isDefaultHerdrTabLabel(tabLabel) && tabLabel !== meta.autoTabTitle
313
+ ? tabLabel
314
+ : undefined;
315
+ infos.push({
316
+ name: meta.name,
317
+ cwd: meta.cwd,
318
+ alive,
319
+ backend: "herdr",
320
+ ...(meta.claudeSessionId !== undefined ? { claudeSessionId: meta.claudeSessionId } : {}),
321
+ ...(meta.agent !== undefined ? { agent: meta.agent } : {}),
322
+ ...(providerSessionId !== undefined ? { providerSessionId } : {}),
323
+ ...(displayTitle !== undefined ? { displayTitle } : {}),
324
+ });
325
+ }
326
+ return infos.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
327
+ }
328
+ /**
329
+ * 生存 pane の session 名 → { tabId, tabLabel } を返す(session-title の自動タイトル同期用)。
330
+ * pane label(= セッション解決の権威)が無い pane・タブ不明の pane は含めない。
331
+ */
332
+ async tabInfoByName() {
333
+ const panes = await this.livePanes();
334
+ const tabs = await this.tabLabels();
335
+ const out = new Map();
336
+ for (const pane of panes) {
337
+ if (pane.label === null || pane.tabId === null)
338
+ continue;
339
+ out.set(pane.label, { tabId: pane.tabId, label: tabs.get(pane.tabId) ?? null });
340
+ }
341
+ return out;
342
+ }
343
+ /** `tab list` を tab_id → label で読む(失敗は空マップ = displayTitle 無しに倒す)。 */
344
+ async tabLabels() {
345
+ try {
346
+ const result = await this.runner(["tab", "list"]);
347
+ if (result.exitCode !== 0)
348
+ return new Map();
349
+ return parseHerdrTabLabels(result.stdout);
350
+ }
351
+ catch {
352
+ return new Map();
353
+ }
354
+ }
355
+ /** 既存セッションへ reattach(生存: attached / 不在: session_not_found エラー封筒)。 */
356
+ async reattach(name) {
357
+ validateSessionName(name);
358
+ const pane = await this.findPane(name);
359
+ if (pane === null) {
360
+ return {
361
+ kind: "notFound",
362
+ error: {
363
+ type: "error",
364
+ v: this.protocolVersion,
365
+ code: "session_not_found",
366
+ message: `セッション '${name}' は存在しません。新規に起動できます。`,
367
+ },
368
+ };
369
+ }
370
+ // Claude が終了してシェルだけ残った pane は入力先として無効(tmux backend と同じ判定)。
371
+ // herdr 起動は `zsh -lc '<cmd>'` で claude 終了と同時に pane が閉じるため通常は起きないが、
372
+ // 想定外にシェルへ戻った pane を生存扱いすると --resume が起動しないため安全側で消す。
373
+ if ((this.store.get(name)?.agent ?? "claude") === "claude" && !(await this.agentProcessAlive(name))) {
374
+ await this.kill(name);
375
+ return {
376
+ kind: "notFound",
377
+ error: {
378
+ type: "error",
379
+ v: this.protocolVersion,
380
+ code: "session_not_found",
381
+ message: `セッション '${name}' のエージェントを再起動します。`,
382
+ },
383
+ };
384
+ }
385
+ const cwd = this.store.get(name)?.cwd ?? "";
386
+ const recent = await this.capturePane(name);
387
+ return { kind: "attached", info: { name, cwd, alive: true, backend: "herdr" }, recentOutput: recent };
388
+ }
389
+ /** pane 内のエージェント生存判定。herdr エラーや空出力は二重起動を避けて true に倒す。 */
390
+ async agentProcessAlive(name) {
391
+ validateSessionName(name);
392
+ const target = await this.paneTarget(name);
393
+ if (target === null)
394
+ return true;
395
+ try {
396
+ const result = await this.runner(["pane", "process-info", "--pane", target]);
397
+ if (result.exitCode !== 0)
398
+ return true;
399
+ const command = parseHerdrForegroundCommand(result.stdout);
400
+ // zsh -lc 起動の実行中は前面が `zsh` に見える瞬間があるため、空/シェル名のみ死亡扱い。
401
+ return paneCommandLooksLikeAgent(command);
402
+ }
403
+ catch {
404
+ return true;
405
+ }
406
+ }
407
+ /**
408
+ * 会話カスタムタイトルの端末表示追随(session-title): pane を収容するタブのラベルを
409
+ * 書き換える(`tab rename`)。pane の label はセッション解決の権威(findPane の
410
+ * fallback / list の生存判定)なので触らない。title 空/null は解除=セッション名へ戻す
411
+ * (未命名扱いになり hub tick の自動タイトルが再適用される)。
412
+ */
413
+ async setDisplayTitle(name, title) {
414
+ validateSessionName(name);
415
+ const pane = await this.findPane(name);
416
+ if (pane === null) {
417
+ throw new HerdrFailedError(["tab", "rename", name], 1, "pane not found");
418
+ }
419
+ if (pane.tabId === null) {
420
+ throw new HerdrFailedError(["tab", "rename", name], 1, "tab id unavailable");
421
+ }
422
+ const trimmed = title?.trim() ?? "";
423
+ const label = trimmed.length > 0 ? trimmed : name;
424
+ const args = ["tab", "rename", pane.tabId, label];
425
+ const result = await this.runner(args);
426
+ if (result.exitCode !== 0) {
427
+ throw new HerdrFailedError(args, result.exitCode, result.stderr);
428
+ }
429
+ }
430
+ /** 指定セッションの pane のみを閉じる(herdr pane close)。 */
431
+ async kill(name) {
432
+ validateSessionName(name);
433
+ const target = await this.paneTarget(name);
434
+ if (target === null) {
435
+ throw new HerdrFailedError(["pane", "close", name], 1, "pane not found");
436
+ }
437
+ const args = ["pane", "close", target];
438
+ const result = await this.runner(args);
439
+ if (result.exitCode !== 0) {
440
+ throw new HerdrFailedError(args, result.exitCode, result.stderr);
441
+ }
442
+ }
443
+ /**
444
+ * 本文入力と送信確定を 1 操作で行う(chat 注入・kick 用, SessionBackend 共通面)。
445
+ * 実測(2026-07-22, jsonl を ground truth に検証):
446
+ * - 本文+CR の単一 send-text は不成立(ペースト末尾の改行として除去され本文が残る)
447
+ * - 分割送信でも claude TUI ブート直後は 600ms 間隔の CR すら飲まれる
448
+ * (アイドル時は 300ms 以上で成立。遅れて送る単発 CR は常に成立)
449
+ * よって「本文 → CR → 入力欄を読んで残留確認 → 残っていれば CR 再送」の確認つき
450
+ * リトライで確定させる。submit 済みの空入力への Enter は no-op なので二重送信は起きない。
451
+ */
452
+ async sendTextSubmit(name, text) {
453
+ // ブート直後の注入は本文ごと TUI 初期化に破棄され得る(実測: 入力欄にも jsonl にも
454
+ // 残らない)。herdr の claude 検出(agent_status が unknown を抜けるまで)を注入の
455
+ // 準備完了ゲートにする。working(処理中の queue 入力)も注入可。判定不能は fail-open。
456
+ await this.waitForAgentReady(name);
457
+ // 選択ダイアログ(/remote-control 等)が開いたままだと本文がダイアログに食われる。
458
+ // 注入前に Esc で閉じてから入力欄へ流す(Mac 側で手動 Esc するのと同じ操作)。
459
+ if (await this.selectionDialogVisible(name)) {
460
+ await this.sendKeys(name, ["Escape"]);
461
+ await new Promise((resolve) => setTimeout(resolve, 300));
462
+ }
463
+ // 中断(停止)直後は claude が queued メッセージを入力欄へ書き戻す。残存したまま
464
+ // 注入すると今回の本文がその後ろへ連結され 1 メッセージとして送信される(実機FB
465
+ // 2026-07-29: 停止→送信で「前回本文+今回本文」の連結バブルが二重表示)。残存は先に
466
+ // Enter で独立メッセージとして送信し切ってから注入する(アプリの楽観バブルとも一致
467
+ // する)。空入力への Enter は no-op なので、誤検出しても無害。
468
+ if (await this.inputBoxHasPendingText(name)) {
469
+ await this.sendKeys(name, ["Enter"]);
470
+ await new Promise((resolve) => setTimeout(resolve, this.submitDelayMs));
471
+ }
472
+ // 前回の注入が途中で終わった等でシェルモード(プロンプト `!`)に入ったままの入力欄へ
473
+ // 注入すると、本文がそのままシェルコマンドとして実行される。空入力の Backspace で
474
+ // モード記号を消して通常入力へ戻してから注入する(`!` 始まりの本文は注入時に自分で
475
+ // シェルモードへ入るので、常に通常モードから始めるのが決定的で安全)。
476
+ await this.exitShellMode(name);
477
+ // Remote Control 切断直後の limbo では typed 入力が入力欄に一切入らず破棄される
478
+ // (実測 2026-07-28: 通常メッセージが本文ごと消失し、旧実装は「入力欄に残っていない
479
+ // = 送信成立」と誤判定して配送済みレシートを発行 → silent loss)。本文が入力欄へ
480
+ // 反映されたことを検証し、反映されなければ間隔を置いて再投入、最終的に throw して
481
+ // chat_send を uncertain(アプリの明示再送)へ倒す。
482
+ const probe = typedTextProbe(text);
483
+ let typed = probe === null;
484
+ for (let attempt = 0; attempt < 3 && !typed; attempt += 1) {
485
+ if (attempt > 0) {
486
+ await new Promise((resolve) => setTimeout(resolve, this.inputRetryDelayMs));
487
+ }
488
+ await this.sendKeys(name, [text], true);
489
+ await new Promise((resolve) => setTimeout(resolve, this.submitDelayMs));
490
+ if (await this.inputBoxContainsText(name, probe)) {
491
+ typed = true;
492
+ break;
493
+ }
494
+ // 描画遅延での誤判定 → 再投入で本文が二重になるのを避けるため、少し待って再確認。
495
+ await new Promise((resolve) => setTimeout(resolve, this.submitVerifyDelayMs));
496
+ if (await this.inputBoxContainsText(name, probe)) {
497
+ typed = true;
498
+ break;
499
+ }
500
+ }
501
+ if (!typed) {
502
+ throw new HerdrFailedError(["pane", "send-text", name], 1, "typed text did not reach the input box (Remote Control 切断直後の limbo の可能性)");
503
+ }
504
+ for (let attempt = 0; attempt < 4; attempt += 1) {
505
+ await new Promise((resolve) => setTimeout(resolve, this.submitDelayMs));
506
+ await this.sendKeys(name, ["Enter"]);
507
+ await new Promise((resolve) => setTimeout(resolve, this.submitVerifyDelayMs));
508
+ // 送出した本文が選択ダイアログを開いた場合(RC active 中の /remote-control 等)、
509
+ // ダイアログのカーソル行(❯ Continue)を未送信テキストと誤認して Enter を再送すると
510
+ // 選択肢を誤操作してダイアログを閉じてしまう(実障害: 転写カードが一瞬で消える)。
511
+ // ダイアログ表示中は送信成立として扱い、以降の操作は転写カード側に任せる。
512
+ if (await this.selectionDialogVisible(name))
513
+ return;
514
+ if (!(await this.inputBoxHasPendingText(name)))
515
+ return;
516
+ }
517
+ }
518
+ /**
519
+ * 入力欄に probe テキストが反映されているか。
520
+ * ペイン履歴の同文エコー(同一メッセージの再送時など)を誤って「入力済み」と
521
+ * 判定しないよう、走査範囲を入力欄(末尾の罫線に挟まれた領域)に限定する。
522
+ * 判定不能は false。
523
+ */
524
+ async inputBoxContainsText(name, probe) {
525
+ try {
526
+ const box = extractClaudeInputBox(await this.captureVisibleScreen(name));
527
+ return box !== null && box.text.includes(probe);
528
+ }
529
+ catch {
530
+ return false;
531
+ }
532
+ }
533
+ /**
534
+ * 入力欄がシェルモード(プロンプト `!`)なら Backspace でモード記号を消し、通常入力へ戻す。
535
+ * 本文が残っている状態では呼ばない(呼び出し元が Enter で送信し切ってから使う)。
536
+ *
537
+ * 判定不能・herdr エラーは no-op(fail-open)。pane 不在などの実エラーはここで throw せず、
538
+ * 続く本文注入の `send-text` 失敗として顕在化させる(この補助操作のエラーを表に出すと
539
+ * 「送信できない」原因が Backspace 送出の失敗に見えてしまう)。
540
+ */
541
+ async exitShellMode(name) {
542
+ try {
543
+ for (let attempt = 0; attempt < 2; attempt += 1) {
544
+ const box = extractClaudeInputBox(await this.captureVisibleScreen(name));
545
+ if (!inputBoxIsShellMode(box) || (box?.text.length ?? 0) > 0)
546
+ return;
547
+ await this.sendKeys(name, ["Backspace"]);
548
+ await new Promise((resolve) => setTimeout(resolve, this.submitVerifyDelayMs));
549
+ }
550
+ }
551
+ catch {
552
+ // no-op(fail-open)
553
+ }
554
+ }
555
+ /**
556
+ * claude TUI の選択ダイアログ(`Enter to select` フッター)が表示中か。判定不能は false。
557
+ *
558
+ * 窓は入力欄判定より狭い 30 行に据え置く(ダイアログのフッターは画面最下部にあるので
559
+ * 広げる必要が無い一方、閉じたダイアログの転写が viewport に残っていると誤検出し、
560
+ * 不要な Esc で入力欄の打ちかけを「送信されないまま」消してしまう)。
561
+ */
562
+ async selectionDialogVisible(name) {
563
+ try {
564
+ return screenHasSelectionFooter(await this.capturePane(name, { lines: 30 }));
565
+ }
566
+ catch {
567
+ return false;
568
+ }
569
+ }
570
+ /**
571
+ * claude TUI の入力準備完了(herdr の agent 検出が unknown を抜ける)を待つ。
572
+ * 0.7.5 で `agent get <name>` は常に agent_not_found(レジストリが herdr 自動検出のみで
573
+ * セッション名を知らない)になり、旧実装は毎送信 10s のタイムアウト空回りになっていた
574
+ * (実機FB 2026-07-30: 送信後の pending / ライブビュー点灯が遅い)。pane を解決して
575
+ * `pane get` の agent_status を読む。pane 不在は即 fail-open(後続 sendKeys が
576
+ * pane not found で正しく失敗する)。
577
+ */
578
+ async waitForAgentReady(name) {
579
+ const deadline = Date.now() + this.readyTimeoutMs;
580
+ for (;;) {
581
+ try {
582
+ const target = await this.paneTarget(name);
583
+ if (target === null)
584
+ return;
585
+ const result = await this.runner(["pane", "get", target]);
586
+ if (result.exitCode === 0) {
587
+ const status = parseHerdrPaneAgentStatus(result.stdout);
588
+ if (status !== null && status !== "unknown")
589
+ return;
590
+ }
591
+ }
592
+ catch {
593
+ return; // herdr 不在等は fail-open(呼び出し側の送出エラーで顕在化させる)
594
+ }
595
+ if (Date.now() > deadline)
596
+ return; // タイムアウトも fail-open
597
+ await new Promise((resolve) => setTimeout(resolve, this.readyPollMs));
598
+ }
599
+ }
600
+ /**
601
+ * claude TUI の入力欄に未送信テキストが残っているか。
602
+ * 送信済みメッセージのエコーも `❯` で始まるため、入力欄(末尾の罫線に挟まれた領域)
603
+ * だけを見る。判定不能(読取失敗・入力欄を特定できない)は false = 送信成立扱い
604
+ * (fail-open。誤リトライしても空入力 Enter の no-op で無害だが、無限再送はしない側に倒す)。
605
+ */
606
+ async inputBoxHasPendingText(name) {
607
+ try {
608
+ const box = extractClaudeInputBox(await this.captureVisibleScreen(name));
609
+ return box !== null && box.text.length > 0;
610
+ }
611
+ catch {
612
+ return false;
613
+ }
614
+ }
615
+ /**
616
+ * 指定セッションの pane へキー/テキストを送出する。
617
+ * literal はテキスト送出。非 literal は herdr が受理するキー名のみ send-keys、
618
+ * BTab は生 Shift+Tab シーケンス、その他(数字キー等)はテキストとして送る。
619
+ */
620
+ async sendKeys(name, keys, literal = false) {
621
+ validateSessionName(name);
622
+ if (keys.length === 0)
623
+ return;
624
+ const target = await this.paneTarget(name);
625
+ if (target === null) {
626
+ throw new HerdrFailedError(["pane", "send-keys", name], 1, "pane not found");
627
+ }
628
+ for (const key of keys) {
629
+ let args;
630
+ if (literal) {
631
+ args = ["pane", "send-text", target, key];
632
+ }
633
+ else if (key === "Enter") {
634
+ // Ink(claude TUI) の submit は CR 必須(send-keys Enter は不認識。上記実測)。
635
+ args = ["pane", "send-text", target, ENTER_SEQUENCE];
636
+ }
637
+ else if (key === "BTab") {
638
+ args = ["pane", "send-text", target, SHIFT_TAB_SEQUENCE];
639
+ }
640
+ else if (HERDR_KEY_NAMES.has(key) || /^C-[a-z]$/.test(key)) {
641
+ args = ["pane", "send-keys", target, key];
642
+ }
643
+ else {
644
+ args = ["pane", "send-text", target, key];
645
+ }
646
+ const result = await this.runner(args);
647
+ if (result.exitCode !== 0) {
648
+ throw new HerdrFailedError(args, result.exitCode, result.stderr);
649
+ }
650
+ }
651
+ }
652
+ /**
653
+ * pane 末尾 N 行を返す(tmux capture-pane 相当。末尾空行は削る)。
654
+ * joinWrappedLines は herdr の recent-unwrapped(折返し結合済み出力リング)を使い、
655
+ * リング未充填の新規 pane では visible(viewport 全行)へフォールバックする。
656
+ */
657
+ async capturePane(name, options = {}) {
658
+ validateSessionName(name);
659
+ const target = await this.paneTarget(name);
660
+ if (target === null) {
661
+ throw new HerdrFailedError(["pane", "read", name], 1, "pane not found");
662
+ }
663
+ const lines = options.lines ?? this.captureLines;
664
+ if (options.joinWrappedLines ?? false) {
665
+ const joined = await this.readPane(target, ["--source", "recent-unwrapped", "--lines", `${lines}`]);
666
+ if (joined.length > 0)
667
+ return joined;
668
+ }
669
+ const visible = await this.readPane(target, ["--source", "visible"]);
670
+ const all = visible.split("\n");
671
+ return all.slice(Math.max(0, all.length - lines)).join("\n");
672
+ }
673
+ /**
674
+ * 入力欄判定用に **viewport 全体**を取る(末尾 N 行で切らない)。
675
+ *
676
+ * claude TUI の入力欄の最大高さは端末サイズにほぼ比例する(実測 2.1.220:
677
+ * 63 行端末で 26 行 / 120 行端末で 55 行)。固定行数の窓では大きな端末で上罫線が
678
+ * 窓外に出て入力欄を見失い、本文を 3 回重ね打ちして送信失敗する。窓を広く取っても
679
+ * `extractClaudeInputBox` は罫線で入力欄を切り出すので過検出にはならない。
680
+ * `--source visible` は viewport 全行を返す(`--lines` は無視される)。
681
+ */
682
+ async captureVisibleScreen(name) {
683
+ validateSessionName(name);
684
+ const target = await this.paneTarget(name);
685
+ if (target === null) {
686
+ throw new HerdrFailedError(["pane", "read", name], 1, "pane not found");
687
+ }
688
+ return this.readPane(target, ["--source", "visible"]);
689
+ }
690
+ async readPane(target, sourceArgs) {
691
+ const args = ["pane", "read", target, ...sourceArgs];
692
+ const result = await this.runner(args);
693
+ if (result.exitCode !== 0) {
694
+ throw new HerdrFailedError(args, result.exitCode, result.stderr);
695
+ }
696
+ const lines = result.stdout.split("\n");
697
+ while (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() === "") {
698
+ lines.pop();
699
+ }
700
+ return lines.join("\n");
701
+ }
702
+ /**
703
+ * tailii セッションの server を pane ゼロのときだけ停止する(reaper の空サーバー回収)。
704
+ * tmux server の「セッションゼロで自動終了」に対応する挙動。launch 側に ensure(不在なら
705
+ * detached 起動)があるため、停止しても次のセッション起動で自動復帰する。
706
+ * ユーザーが tailii セッション内に手動 pane を作っていた場合は停止しない(pane 総数で判定)。
707
+ */
708
+ async stopServerIfEmpty() {
709
+ try {
710
+ const result = await this.runner(["pane", "list"]);
711
+ if (result.exitCode !== 0)
712
+ return; // server 不在/不通 = 何もしない
713
+ if (parseHerdrPaneList(result.stdout).length > 0)
714
+ return;
715
+ await this.runner(["server", "stop"]);
716
+ }
717
+ catch {
718
+ // ENOENT 等(未導入環境)は無視。
719
+ }
720
+ }
721
+ /** 生存 pane の一覧。herdr server 未起動などの失敗は空集合として扱う(tmux `ls` と同じ流儀)。 */
722
+ async livePanes() {
723
+ try {
724
+ const result = await this.runner(["pane", "list"]);
725
+ if (result.exitCode !== 0)
726
+ return [];
727
+ return parseHerdrPaneList(result.stdout);
728
+ }
729
+ catch {
730
+ return [];
731
+ }
732
+ }
733
+ /** セッション名の現存 pane を解決する(記録済み pane ID 優先、無ければ label 一致)。 */
734
+ async findPane(name) {
735
+ const panes = await this.livePanes();
736
+ const recorded = this.store.get(name)?.herdrPaneId;
737
+ if (recorded !== undefined) {
738
+ const byId = panes.find((pane) => pane.paneId === recorded);
739
+ if (byId !== undefined)
740
+ return byId;
741
+ }
742
+ return panes.find((pane) => pane.label === name) ?? null;
743
+ }
744
+ /** 入出力 target の pane ID(現存しなければ null)。 */
745
+ async paneTarget(name) {
746
+ return (await this.findPane(name))?.paneId ?? null;
747
+ }
748
+ }
749
+ //# sourceMappingURL=herdr.js.map