@jackwener/opencli 1.7.12 → 1.7.13

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 (407) hide show
  1. package/README.md +8 -7
  2. package/README.zh-CN.md +9 -8
  3. package/cli-manifest.json +12194 -6843
  4. package/clis/1point3acres/digest.js +35 -0
  5. package/clis/1point3acres/forum.js +51 -0
  6. package/clis/1point3acres/forums.js +44 -0
  7. package/clis/1point3acres/hot.js +35 -0
  8. package/clis/1point3acres/latest.js +35 -0
  9. package/clis/1point3acres/notifications.js +64 -0
  10. package/clis/1point3acres/search.js +71 -0
  11. package/clis/1point3acres/thread.js +117 -0
  12. package/clis/1point3acres/user.js +77 -0
  13. package/clis/1point3acres/utils.js +247 -0
  14. package/clis/_shared/desktop-commands.js +4 -0
  15. package/clis/aibase/news.js +110 -0
  16. package/clis/aibase/news.test.js +59 -0
  17. package/clis/amazon/discussion.test.js +1 -28
  18. package/clis/antigravity/watch.js +3 -2
  19. package/clis/arxiv/author.js +44 -0
  20. package/clis/baidu-scholar/search.js +0 -1
  21. package/clis/bbc/topic.js +57 -0
  22. package/clis/bbc/utils.js +79 -0
  23. package/clis/chaoxing/assignments.js +1 -1
  24. package/clis/chaoxing/exams.js +1 -1
  25. package/clis/chatgpt/ask.js +57 -0
  26. package/clis/chatgpt/commands.test.js +45 -0
  27. package/clis/chatgpt/detail.js +46 -0
  28. package/clis/chatgpt/history.js +39 -0
  29. package/clis/chatgpt/image.js +12 -11
  30. package/clis/chatgpt/image.test.js +23 -0
  31. package/clis/chatgpt/new.js +25 -0
  32. package/clis/chatgpt/read.js +43 -0
  33. package/clis/chatgpt/send.js +46 -0
  34. package/clis/chatgpt/status.js +29 -0
  35. package/clis/chatgpt/utils.js +294 -4
  36. package/clis/chatgpt/utils.test.js +13 -0
  37. package/clis/chatgpt-app/ask.js +6 -3
  38. package/clis/chatwise/ask.js +16 -43
  39. package/clis/chatwise/composer.test.js +186 -0
  40. package/clis/chatwise/send.js +2 -24
  41. package/clis/chatwise/utils.js +143 -0
  42. package/clis/claude/ask.js +1 -1
  43. package/clis/claude/detail.js +1 -0
  44. package/clis/claude/history.js +1 -0
  45. package/clis/claude/new.js +1 -0
  46. package/clis/claude/read.js +1 -0
  47. package/clis/claude/send.js +1 -0
  48. package/clis/claude/status.js +1 -0
  49. package/clis/codex/ask.js +15 -9
  50. package/clis/codex/history.js +16 -33
  51. package/clis/codex/projects.js +28 -0
  52. package/clis/codex/read.js +10 -4
  53. package/clis/codex/send.js +10 -3
  54. package/clis/codex/sidebar.js +356 -0
  55. package/clis/codex/sidebar.test.js +329 -0
  56. package/clis/coingecko/categories.js +75 -0
  57. package/clis/coingecko/coin.js +107 -0
  58. package/clis/coingecko/coingecko.test.js +109 -0
  59. package/clis/coingecko/derivatives.js +84 -0
  60. package/clis/coingecko/exchanges.js +74 -0
  61. package/clis/coingecko/global.js +71 -0
  62. package/clis/coingecko/top.js +64 -0
  63. package/clis/coingecko/trending.js +55 -0
  64. package/clis/coupang/add-to-cart.js +21 -13
  65. package/clis/coupang/coupang.test.js +159 -0
  66. package/clis/coupang/product.js +257 -0
  67. package/clis/coupang/search.js +38 -16
  68. package/clis/coupang/utils.js +55 -1
  69. package/clis/crates/crate.js +62 -0
  70. package/clis/crates/search.js +44 -0
  71. package/clis/crates/utils.js +72 -0
  72. package/clis/ctrip/ctrip.test.js +234 -0
  73. package/clis/ctrip/hotel-suggest.js +45 -0
  74. package/clis/ctrip/search.js +22 -68
  75. package/clis/ctrip/utils.js +175 -0
  76. package/clis/cursor/ask.js +6 -3
  77. package/clis/dblp/author.js +133 -0
  78. package/clis/dblp/venue.js +64 -0
  79. package/clis/deepseek/ask.js +12 -7
  80. package/clis/deepseek/ask.test.js +13 -13
  81. package/clis/deepseek/detail.js +38 -0
  82. package/clis/deepseek/detail.test.js +81 -0
  83. package/clis/deepseek/history.js +1 -0
  84. package/clis/deepseek/new.js +1 -0
  85. package/clis/deepseek/read.js +1 -0
  86. package/clis/deepseek/send.js +140 -0
  87. package/clis/deepseek/send.test.js +107 -0
  88. package/clis/deepseek/status.js +1 -0
  89. package/clis/deepseek/utils.js +66 -0
  90. package/clis/deepseek/utils.test.js +107 -1
  91. package/clis/defillama/defillama.test.js +99 -0
  92. package/clis/defillama/protocol.js +84 -0
  93. package/clis/defillama/protocols.js +55 -0
  94. package/clis/defillama/utils.js +99 -0
  95. package/clis/devto/latest.js +74 -0
  96. package/clis/dockerhub/image.js +52 -0
  97. package/clis/dockerhub/search.js +47 -0
  98. package/clis/dockerhub/utils.js +100 -0
  99. package/clis/doubao/ask.js +7 -3
  100. package/clis/doubao/detail.js +1 -0
  101. package/clis/doubao/history.js +1 -0
  102. package/clis/doubao/meeting-summary.js +1 -0
  103. package/clis/doubao/meeting-transcript.js +1 -0
  104. package/clis/doubao/new.js +1 -0
  105. package/clis/doubao/read.js +1 -0
  106. package/clis/doubao/send.js +1 -0
  107. package/clis/doubao/status.js +1 -0
  108. package/clis/douyin/draft.test.js +1 -30
  109. package/clis/endoflife/endoflife.test.js +51 -0
  110. package/clis/endoflife/product.js +55 -0
  111. package/clis/endoflife/utils.js +89 -0
  112. package/clis/facebook/__fixtures__/notifications-page.html +13 -0
  113. package/clis/facebook/notifications.js +326 -30
  114. package/clis/facebook/notifications.test.js +458 -0
  115. package/clis/flathub/app.js +71 -0
  116. package/clis/flathub/flathub.test.js +90 -0
  117. package/clis/flathub/search.js +80 -0
  118. package/clis/flathub/utils.js +114 -0
  119. package/clis/gemini/ask.js +7 -3
  120. package/clis/gemini/ask.test.js +2 -2
  121. package/clis/gemini/deep-research-result.js +6 -2
  122. package/clis/gemini/deep-research-result.test.js +15 -14
  123. package/clis/gemini/deep-research.js +8 -4
  124. package/clis/gemini/deep-research.test.js +15 -18
  125. package/clis/gemini/image.js +7 -2
  126. package/clis/gemini/new.js +1 -0
  127. package/clis/gemini/utils.js +0 -4
  128. package/clis/google-scholar/cite.js +0 -1
  129. package/clis/google-scholar/profile.js +0 -1
  130. package/clis/google-scholar/search.js +0 -1
  131. package/clis/goproxy/goproxy.test.js +103 -0
  132. package/clis/goproxy/module.js +47 -0
  133. package/clis/goproxy/utils.js +165 -0
  134. package/clis/goproxy/versions.js +59 -0
  135. package/clis/gov-law/recent.js +0 -1
  136. package/clis/gov-law/search.js +0 -1
  137. package/clis/gov-policy/__fixtures__/recent.html +16 -0
  138. package/clis/gov-policy/__fixtures__/search.html +41 -0
  139. package/clis/gov-policy/gov-policy.test.js +224 -0
  140. package/clis/gov-policy/recent.js +66 -24
  141. package/clis/gov-policy/search.js +65 -23
  142. package/clis/gov-policy/utils.js +54 -0
  143. package/clis/grok/ask.js +49 -265
  144. package/clis/grok/ask.test.js +21 -46
  145. package/clis/grok/detail.js +60 -0
  146. package/clis/grok/history.js +48 -0
  147. package/clis/grok/{image.ts → image.js} +56 -70
  148. package/clis/grok/image.test.ts +20 -0
  149. package/clis/grok/new.js +20 -0
  150. package/clis/grok/read.js +39 -0
  151. package/clis/grok/send.js +50 -0
  152. package/clis/grok/status.js +41 -0
  153. package/clis/grok/utils.js +326 -0
  154. package/clis/grok/utils.test.js +103 -0
  155. package/clis/hf/datasets.js +88 -0
  156. package/clis/hf/hf.test.js +16 -0
  157. package/clis/hf/models.js +91 -0
  158. package/clis/hf/paper.js +79 -0
  159. package/clis/hf/spaces.js +101 -0
  160. package/clis/hf/top.js +1 -0
  161. package/clis/homebrew/cask.js +39 -0
  162. package/clis/homebrew/formula.js +41 -0
  163. package/clis/homebrew/popular.js +54 -0
  164. package/clis/homebrew/utils.js +100 -0
  165. package/clis/hupu/__fixtures__/hot-home.html +64 -0
  166. package/clis/hupu/detail.js +0 -1
  167. package/clis/hupu/hot.js +156 -35
  168. package/clis/hupu/hot.test.js +224 -0
  169. package/clis/hupu/search.js +0 -1
  170. package/clis/instagram/note.js +1 -1
  171. package/clis/instagram/note.test.js +1 -29
  172. package/clis/instagram/post.js +1 -1
  173. package/clis/instagram/post.test.js +1 -1
  174. package/clis/instagram/reel.js +1 -1
  175. package/clis/instagram/story.js +1 -1
  176. package/clis/instagram/story.test.js +1 -34
  177. package/clis/jd/commands.test.js +1 -24
  178. package/clis/lichess/lichess.test.js +85 -0
  179. package/clis/lichess/top.js +46 -0
  180. package/clis/lichess/user.js +91 -0
  181. package/clis/lichess/utils.js +97 -0
  182. package/clis/linkedin/search.js +107 -10
  183. package/clis/linkedin/search.test.js +222 -0
  184. package/clis/linux-do/feed.js +2 -5
  185. package/clis/linux-do/feed.test.js +35 -0
  186. package/clis/lobsters/domain.js +92 -0
  187. package/clis/maven/artifact.js +49 -0
  188. package/clis/maven/search.js +51 -0
  189. package/clis/maven/utils.js +110 -0
  190. package/clis/mdn/search.js +97 -0
  191. package/clis/medium/tag.js +135 -0
  192. package/clis/npm/downloads.js +59 -0
  193. package/clis/npm/package.js +70 -0
  194. package/clis/npm/search.js +49 -0
  195. package/clis/npm/utils.js +76 -0
  196. package/clis/nuget/nuget.test.js +111 -0
  197. package/clis/nuget/package.js +101 -0
  198. package/clis/nuget/search.js +69 -0
  199. package/clis/nuget/utils.js +87 -0
  200. package/clis/nvd/cve.js +121 -0
  201. package/clis/oeis/oeis.test.js +88 -0
  202. package/clis/oeis/search.js +63 -0
  203. package/clis/oeis/sequence.js +71 -0
  204. package/clis/oeis/utils.js +88 -0
  205. package/clis/openalex/search.js +69 -0
  206. package/clis/openalex/utils.js +160 -0
  207. package/clis/openalex/work.js +65 -0
  208. package/clis/openfda/drug-label.js +74 -0
  209. package/clis/openfda/food-recall.js +65 -0
  210. package/clis/openfda/openfda.test.js +114 -0
  211. package/clis/openfda/utils.js +67 -0
  212. package/clis/osv/osv.test.js +97 -0
  213. package/clis/osv/query.js +72 -0
  214. package/clis/osv/utils.js +169 -0
  215. package/clis/osv/vulnerability.js +54 -0
  216. package/clis/packagist/package.js +49 -0
  217. package/clis/packagist/search.js +43 -0
  218. package/clis/packagist/utils.js +113 -0
  219. package/clis/paperreview/feedback.js +1 -1
  220. package/clis/paperreview/review.js +1 -1
  221. package/clis/paperreview/submit.js +1 -1
  222. package/clis/pixiv/download.test.js +1 -1
  223. package/clis/pixiv/illusts.test.js +1 -1
  224. package/clis/pixiv/search.test.js +1 -1
  225. package/clis/pubmed/article.js +50 -0
  226. package/clis/pubmed/author.js +64 -0
  227. package/clis/pubmed/citations.js +36 -0
  228. package/clis/pubmed/pubmed.test.js +276 -0
  229. package/clis/pubmed/related.js +45 -0
  230. package/clis/pubmed/search.js +75 -0
  231. package/clis/pubmed/utils.js +309 -0
  232. package/clis/pypi/downloads.js +66 -0
  233. package/clis/pypi/package.js +79 -0
  234. package/clis/pypi/utils.js +55 -0
  235. package/clis/quark/mv.js +1 -1
  236. package/clis/quark/save.js +1 -1
  237. package/clis/qwen/ask.js +85 -0
  238. package/clis/qwen/detail.js +62 -0
  239. package/clis/qwen/history.js +61 -0
  240. package/clis/qwen/image.js +179 -0
  241. package/clis/qwen/new.js +23 -0
  242. package/clis/qwen/read.js +41 -0
  243. package/clis/qwen/send.js +55 -0
  244. package/clis/qwen/status.js +37 -0
  245. package/clis/qwen/utils.js +409 -0
  246. package/clis/qwen/utils.test.js +45 -0
  247. package/clis/rest-countries/country.js +65 -0
  248. package/clis/rest-countries/region.js +64 -0
  249. package/clis/rest-countries/rest-countries.test.js +83 -0
  250. package/clis/rest-countries/utils.js +126 -0
  251. package/clis/reuters/article-detail.js +53 -0
  252. package/clis/reuters/reuters.test.js +299 -0
  253. package/clis/reuters/search.js +45 -34
  254. package/clis/reuters/utils.js +159 -0
  255. package/clis/rfc/rfc.js +52 -0
  256. package/clis/rfc/rfc.test.js +74 -0
  257. package/clis/rfc/utils.js +72 -0
  258. package/clis/rubygems/gem.js +42 -0
  259. package/clis/rubygems/search.js +47 -0
  260. package/clis/rubygems/utils.js +86 -0
  261. package/clis/stackoverflow/related.js +66 -0
  262. package/clis/stackoverflow/stackoverflow.test.js +58 -0
  263. package/clis/stackoverflow/tag.js +60 -0
  264. package/clis/stackoverflow/user.js +50 -0
  265. package/clis/stackoverflow/utils.js +118 -0
  266. package/clis/steam/app.js +67 -0
  267. package/clis/steam/search.js +58 -0
  268. package/clis/steam/steam.test.js +46 -0
  269. package/clis/steam/utils.js +107 -0
  270. package/clis/taobao/commands.test.js +1 -24
  271. package/clis/test-utils.js +61 -0
  272. package/clis/tieba/hot.js +0 -1
  273. package/clis/tiktok/comment.js +128 -41
  274. package/clis/tiktok/creator-videos.js +270 -0
  275. package/clis/tiktok/creator-videos.test.js +113 -0
  276. package/clis/tiktok/explore.js +137 -29
  277. package/clis/tiktok/follow.js +115 -33
  278. package/clis/tiktok/following.js +157 -36
  279. package/clis/tiktok/friends.js +139 -37
  280. package/clis/tiktok/live.js +137 -41
  281. package/clis/tiktok/notifications.js +141 -38
  282. package/clis/tiktok/refactor.test.js +389 -0
  283. package/clis/tiktok/unfollow.js +124 -38
  284. package/clis/tiktok/user.js +203 -29
  285. package/clis/tiktok/utils.js +505 -0
  286. package/clis/tiktok/write-refactor.test.js +370 -0
  287. package/clis/toutiao/articles.js +36 -62
  288. package/clis/toutiao/hot.js +63 -0
  289. package/clis/toutiao/toutiao.test.js +378 -0
  290. package/clis/toutiao/utils.js +161 -0
  291. package/clis/tvmaze/search.js +61 -0
  292. package/clis/tvmaze/show.js +60 -0
  293. package/clis/tvmaze/tvmaze.test.js +93 -0
  294. package/clis/tvmaze/utils.js +110 -0
  295. package/clis/twitter/accept.js +1 -1
  296. package/clis/twitter/followers.js +134 -69
  297. package/clis/twitter/reply-dm.js +1 -1
  298. package/clis/twitter/reply.test.js +1 -29
  299. package/clis/uisdc/news.js +105 -0
  300. package/clis/uisdc/news.test.js +66 -0
  301. package/clis/wanfang/search.js +0 -1
  302. package/clis/web/read.js +47 -17
  303. package/clis/web/read.test.js +101 -1
  304. package/clis/weixin/create-draft.js +1 -1
  305. package/clis/weixin/drafts.js +1 -1
  306. package/clis/weixin/drafts.test.js +5 -1
  307. package/clis/weixin/search.js +157 -0
  308. package/clis/weixin/search.test.js +227 -0
  309. package/clis/wikidata/entity.js +60 -0
  310. package/clis/wikidata/search.js +50 -0
  311. package/clis/wikidata/utils.js +117 -0
  312. package/clis/wikidata/wikidata.test.js +83 -0
  313. package/clis/wikipedia/page.js +95 -0
  314. package/clis/wttr/current.js +63 -0
  315. package/clis/wttr/forecast.js +71 -0
  316. package/clis/wttr/utils.js +50 -0
  317. package/clis/wttr/wttr.test.js +84 -0
  318. package/clis/xianyu/chat.js +16 -4
  319. package/clis/xianyu/chat.test.js +64 -0
  320. package/clis/xianyu/publish.js +485 -0
  321. package/clis/xianyu/publish.test.js +220 -0
  322. package/clis/xiaoe/catalog.js +105 -40
  323. package/clis/xiaoe/content.js +164 -29
  324. package/clis/xiaoe/courses.js +86 -29
  325. package/clis/xiaoe/xiaoe.test.js +486 -0
  326. package/clis/xiaohongshu/creator-notes-summary.js +1 -1
  327. package/clis/xiaohongshu/publish.js +16 -3
  328. package/clis/xiaohongshu/publish.test.js +46 -1
  329. package/clis/youtube/transcript.js +13 -19
  330. package/clis/youtube/transcript.test.js +17 -0
  331. package/clis/yuanbao/ask.js +17 -66
  332. package/clis/yuanbao/ask.test.js +5 -5
  333. package/clis/yuanbao/detail.js +65 -0
  334. package/clis/yuanbao/history.js +51 -0
  335. package/clis/yuanbao/new.js +1 -0
  336. package/clis/yuanbao/read.js +38 -0
  337. package/clis/yuanbao/send.js +57 -0
  338. package/clis/yuanbao/shared.js +297 -5
  339. package/clis/yuanbao/shared.test.js +80 -0
  340. package/clis/yuanbao/status.js +44 -0
  341. package/clis/zlibrary/commands.test.js +1 -11
  342. package/dist/src/browser/base-page.d.ts +9 -0
  343. package/dist/src/browser/base-page.js +44 -1
  344. package/dist/src/browser/base-page.test.js +66 -0
  345. package/dist/src/browser/cdp.d.ts +1 -0
  346. package/dist/src/browser/cdp.js +51 -9
  347. package/dist/src/browser/daemon-client.d.ts +4 -0
  348. package/dist/src/browser/errors.js +1 -1
  349. package/dist/src/browser/page.d.ts +1 -1
  350. package/dist/src/browser/page.js +3 -1
  351. package/dist/src/browser/page.test.js +29 -0
  352. package/dist/src/browser/target-errors.d.ts +2 -1
  353. package/dist/src/browser/target-errors.js +1 -0
  354. package/dist/src/browser/target-resolver.d.ts +25 -0
  355. package/dist/src/browser/target-resolver.js +43 -0
  356. package/dist/src/build-manifest.js +9 -4
  357. package/dist/src/build-manifest.test.js +2 -8
  358. package/dist/src/capabilityRouting.d.ts +16 -1
  359. package/dist/src/capabilityRouting.js +24 -1
  360. package/dist/src/capabilityRouting.test.js +19 -1
  361. package/dist/src/cli.js +76 -11
  362. package/dist/src/cli.test.js +150 -0
  363. package/dist/src/commanderAdapter.js +0 -5
  364. package/dist/src/commanderAdapter.test.js +0 -1
  365. package/dist/src/discovery.js +2 -5
  366. package/dist/src/errors.js +1 -1
  367. package/dist/src/execution.d.ts +1 -1
  368. package/dist/src/execution.js +111 -27
  369. package/dist/src/execution.test.js +326 -17
  370. package/dist/src/help.d.ts +23 -2
  371. package/dist/src/help.js +41 -19
  372. package/dist/src/help.test.d.ts +1 -0
  373. package/dist/src/help.test.js +54 -0
  374. package/dist/src/main.js +14 -1
  375. package/dist/src/manifest-types.d.ts +5 -3
  376. package/dist/src/pipeline/executor.js +1 -1
  377. package/dist/src/pipeline/executor.test.js +8 -0
  378. package/dist/src/pipeline/registry.d.ts +9 -0
  379. package/dist/src/pipeline/registry.js +13 -1
  380. package/dist/src/pipeline/steps/browser.d.ts +1 -0
  381. package/dist/src/pipeline/steps/browser.js +10 -0
  382. package/dist/src/pipeline/steps/download.test.js +1 -0
  383. package/dist/src/registry-api.d.ts +1 -1
  384. package/dist/src/registry.d.ts +12 -11
  385. package/dist/src/registry.js +16 -6
  386. package/dist/src/registry.test.js +2 -2
  387. package/dist/src/runtime.d.ts +2 -1
  388. package/dist/src/runtime.js +1 -1
  389. package/dist/src/serialization.d.ts +2 -2
  390. package/dist/src/serialization.js +4 -6
  391. package/dist/src/serialization.test.js +17 -0
  392. package/dist/src/types.d.ts +17 -0
  393. package/dist/src/validate.js +15 -11
  394. package/dist/src/validate.test.d.ts +9 -0
  395. package/dist/src/validate.test.js +90 -0
  396. package/package.json +1 -1
  397. package/scripts/fetch-adapters.js +1 -1
  398. package/scripts/typed-error-lint-baseline.json +5 -77
  399. package/clis/ctrip/search.test.js +0 -64
  400. package/clis/gov-policy/commands.test.js +0 -27
  401. package/clis/linux-do/category.js +0 -37
  402. package/clis/linux-do/hot.js +0 -26
  403. package/clis/linux-do/latest.js +0 -19
  404. package/clis/pixiv/test-utils.js +0 -23
  405. package/clis/toutiao/articles.test.js +0 -30
  406. package/dist/src/analysis.d.ts +0 -40
  407. package/dist/src/analysis.js +0 -172
@@ -0,0 +1,356 @@
1
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
2
+
3
+ function cleanText(value) {
4
+ return String(value ?? '').replace(/\s+/g, ' ').trim();
5
+ }
6
+
7
+ function normalizeMatch(value) {
8
+ return cleanText(value).toLowerCase();
9
+ }
10
+
11
+ function matchesProject(project, query) {
12
+ if (!query)
13
+ return true;
14
+ const label = normalizeMatch(project.project);
15
+ const projectPath = normalizeMatch(project.projectPath);
16
+ const needle = normalizeMatch(query);
17
+ if (!needle)
18
+ return true;
19
+ return label === needle
20
+ || label.includes(needle)
21
+ || projectPath === needle
22
+ || projectPath.endsWith(`/${needle}`);
23
+ }
24
+
25
+ export function hasConversationTarget(kwargs) {
26
+ return !!(kwargs?.project || kwargs?.conversation || kwargs?.index || kwargs?.['thread-id']);
27
+ }
28
+
29
+ export function parsePositiveIntegerOption(raw, label) {
30
+ const value = cleanText(raw);
31
+ if (!/^\d+$/.test(value)) {
32
+ throw new ArgumentError(`${label} must be a positive integer`);
33
+ }
34
+ const parsed = Number.parseInt(value, 10);
35
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
36
+ throw new ArgumentError(`${label} must be a positive integer`);
37
+ }
38
+ return parsed;
39
+ }
40
+
41
+ export function parseOptionalPositiveIntegerOption(raw, label) {
42
+ if (raw == null || cleanText(raw) === '') {
43
+ return null;
44
+ }
45
+ return parsePositiveIntegerOption(raw, label);
46
+ }
47
+
48
+ export function requireNonEmptyOption(raw, label) {
49
+ const value = cleanText(raw);
50
+ if (!value) {
51
+ throw new ArgumentError(`${label} cannot be empty`);
52
+ }
53
+ return value;
54
+ }
55
+
56
+ export function collectCodexProjectsFromDocument(doc = document) {
57
+ const projectRowSelector = '[data-app-action-sidebar-project-row]';
58
+ const threadRowSelector = '[data-app-action-sidebar-thread-row]';
59
+
60
+ function visibleText(el) {
61
+ return (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim();
62
+ }
63
+
64
+ function isRelativeTime(text) {
65
+ return /^(?:(?:\d+\s*)?(?:刚刚|秒|分钟|小时|天|周|个月|年|sec|min|hr|hour|day|week|month|year|s|m|h|d|w)|.*\bago)$/i.test(text.trim());
66
+ }
67
+
68
+ function getUpdatedText(row, title) {
69
+ const candidates = Array.from(row.querySelectorAll('.tabular-nums, [class*="tabular-nums"], [class*="description"]'))
70
+ .map(visibleText)
71
+ .filter(Boolean);
72
+ const direct = candidates.find(isRelativeTime);
73
+ if (direct)
74
+ return direct;
75
+ const fullText = visibleText(row);
76
+ const suffix = fullText.replace(title, '').trim();
77
+ return isRelativeTime(suffix) ? suffix : '';
78
+ }
79
+
80
+ return Array.from(doc.querySelectorAll(projectRowSelector)).map((projectRow, projectIndex) => {
81
+ const label = projectRow.getAttribute('data-app-action-sidebar-project-label')
82
+ || projectRow.getAttribute('aria-label')
83
+ || visibleText(projectRow);
84
+ const path = projectRow.getAttribute('data-app-action-sidebar-project-id') || '';
85
+ const projectItem = projectRow.closest('[role="listitem"][aria-label]') || projectRow.parentElement;
86
+ const threadRows = projectItem
87
+ ? Array.from(projectItem.querySelectorAll(threadRowSelector))
88
+ : [];
89
+ const conversations = threadRows.map((row, index) => {
90
+ const title = row.getAttribute('data-app-action-sidebar-thread-title') || visibleText(row);
91
+ return {
92
+ index: index + 1,
93
+ title,
94
+ updated: getUpdatedText(row, title),
95
+ active: row.getAttribute('data-app-action-sidebar-thread-active') === 'true',
96
+ pinned: row.getAttribute('data-app-action-sidebar-thread-pinned') === 'true',
97
+ threadId: row.getAttribute('data-app-action-sidebar-thread-id') || '',
98
+ hostId: row.getAttribute('data-app-action-sidebar-thread-host-id') || '',
99
+ kind: row.getAttribute('data-app-action-sidebar-thread-kind') || '',
100
+ };
101
+ });
102
+
103
+ return {
104
+ index: projectIndex + 1,
105
+ project: label,
106
+ projectPath: path,
107
+ collapsed: projectRow.getAttribute('data-app-action-sidebar-project-collapsed') === 'true'
108
+ || projectRow.getAttribute('aria-expanded') === 'false',
109
+ conversations,
110
+ };
111
+ });
112
+ }
113
+
114
+ export function selectCodexConversationInDocument(target, doc = document) {
115
+ const projectRowSelector = '[data-app-action-sidebar-project-row]';
116
+ const threadRowSelector = '[data-app-action-sidebar-thread-row]';
117
+
118
+ function clean(value) {
119
+ return String(value ?? '').replace(/\s+/g, ' ').trim();
120
+ }
121
+
122
+ function normalize(value) {
123
+ return clean(value).toLowerCase();
124
+ }
125
+
126
+ function matches(value, query) {
127
+ const haystack = normalize(value);
128
+ const needle = normalize(query);
129
+ return !!needle && (haystack === needle || haystack.includes(needle));
130
+ }
131
+
132
+ function projectLabel(row) {
133
+ return row.getAttribute('data-app-action-sidebar-project-label')
134
+ || row.getAttribute('aria-label')
135
+ || row.textContent
136
+ || '';
137
+ }
138
+
139
+ function projectPath(row) {
140
+ return row.getAttribute('data-app-action-sidebar-project-id') || '';
141
+ }
142
+
143
+ function threadTitle(row) {
144
+ return row.getAttribute('data-app-action-sidebar-thread-title')
145
+ || row.textContent
146
+ || '';
147
+ }
148
+
149
+ function centerPoint(row) {
150
+ const rect = row.getBoundingClientRect?.();
151
+ if (!rect || !Number.isFinite(rect.left) || !Number.isFinite(rect.top)) {
152
+ return {};
153
+ }
154
+ return {
155
+ x: rect.left + rect.width / 2,
156
+ y: rect.top + rect.height / 2,
157
+ };
158
+ }
159
+
160
+ const projectRows = Array.from(doc.querySelectorAll(projectRowSelector));
161
+ let projectRow = null;
162
+ if (target.project) {
163
+ projectRow = projectRows.find(row => matches(projectLabel(row), target.project))
164
+ || projectRows.find(row => matches(projectPath(row), target.project));
165
+ if (!projectRow) {
166
+ return {
167
+ ok: false,
168
+ error: `Project not found: ${target.project}`,
169
+ projects: projectRows.map(row => projectLabel(row)).filter(Boolean),
170
+ };
171
+ }
172
+ }
173
+
174
+ if (projectRow) {
175
+ const collapsed = projectRow.getAttribute('data-app-action-sidebar-project-collapsed') === 'true'
176
+ || projectRow.getAttribute('aria-expanded') === 'false';
177
+ if (collapsed) {
178
+ projectRow.scrollIntoView?.({ block: 'center' });
179
+ if (!target.preferNativeClick) {
180
+ projectRow.click();
181
+ }
182
+ return {
183
+ ok: true,
184
+ expanded: true,
185
+ selected: false,
186
+ project: projectLabel(projectRow),
187
+ projectPath: projectPath(projectRow),
188
+ ...centerPoint(projectRow),
189
+ };
190
+ }
191
+ }
192
+
193
+ const scope = projectRow
194
+ ? (projectRow.closest('[role="listitem"][aria-label]') || projectRow.parentElement || doc)
195
+ : doc;
196
+ const threadRows = Array.from(scope.querySelectorAll(threadRowSelector));
197
+ if (!threadRows.length) {
198
+ return {
199
+ ok: false,
200
+ error: projectRow
201
+ ? `No visible conversations under project: ${projectLabel(projectRow)}`
202
+ : 'No visible Codex conversations found',
203
+ };
204
+ }
205
+
206
+ let threadRow = null;
207
+ if (target.threadId) {
208
+ threadRow = threadRows.find(row => row.getAttribute('data-app-action-sidebar-thread-id') === target.threadId);
209
+ }
210
+ if (!threadRow && target.index != null) {
211
+ const index = Number(target.index);
212
+ if (!Number.isInteger(index) || index < 1) {
213
+ return { ok: false, error: `Invalid conversation index: ${target.index}` };
214
+ }
215
+ threadRow = threadRows[index - 1] || null;
216
+ }
217
+ if (!threadRow && target.conversation) {
218
+ const exact = threadRows.filter(row => normalize(threadTitle(row)) === normalize(target.conversation));
219
+ threadRow = exact[0] || threadRows.find(row => matches(threadTitle(row), target.conversation)) || null;
220
+ if (exact.length > 1 && !target.project) {
221
+ return {
222
+ ok: false,
223
+ error: `Multiple conversations matched "${target.conversation}". Pass --project to disambiguate.`,
224
+ };
225
+ }
226
+ }
227
+
228
+ if (!threadRow) {
229
+ return {
230
+ ok: false,
231
+ error: target.threadId
232
+ ? `Thread not found: ${target.threadId}`
233
+ : target.conversation
234
+ ? `Conversation not found: ${target.conversation}`
235
+ : 'Pass --conversation, --thread-id, or --index to select a Codex conversation',
236
+ conversations: threadRows.map((row, index) => ({ index: index + 1, title: threadTitle(row) })),
237
+ };
238
+ }
239
+
240
+ threadRow.scrollIntoView?.({ block: 'center' });
241
+ if (!target.preferNativeClick) {
242
+ threadRow.click();
243
+ }
244
+ return {
245
+ ok: true,
246
+ expanded: false,
247
+ selected: true,
248
+ project: projectRow ? projectLabel(projectRow) : '',
249
+ projectPath: projectRow ? projectPath(projectRow) : '',
250
+ conversation: threadTitle(threadRow),
251
+ threadId: threadRow.getAttribute('data-app-action-sidebar-thread-id') || '',
252
+ index: threadRows.indexOf(threadRow) + 1,
253
+ ...centerPoint(threadRow),
254
+ };
255
+ }
256
+
257
+ export function flattenCodexProjects(projects, opts = {}) {
258
+ const projectFilter = opts.project;
259
+ const limit = parseOptionalPositiveIntegerOption(opts.limit, 'codex --limit');
260
+ const rows = [];
261
+ for (const project of projects) {
262
+ if (!matchesProject(project, projectFilter)) {
263
+ continue;
264
+ }
265
+ const conversations = limit
266
+ ? project.conversations.slice(0, limit)
267
+ : project.conversations;
268
+ if (conversations.length === 0) {
269
+ rows.push({
270
+ Project: project.project,
271
+ Index: 0,
272
+ Title: project.collapsed ? '(collapsed)' : '(no visible conversations)',
273
+ Updated: '',
274
+ Active: '',
275
+ ProjectPath: project.projectPath,
276
+ ThreadId: '',
277
+ });
278
+ continue;
279
+ }
280
+ for (const conversation of conversations) {
281
+ rows.push({
282
+ Project: project.project,
283
+ Index: conversation.index,
284
+ Title: conversation.title,
285
+ Updated: conversation.updated,
286
+ Active: conversation.active ? 'yes' : '',
287
+ ProjectPath: project.projectPath,
288
+ ThreadId: conversation.threadId,
289
+ });
290
+ }
291
+ }
292
+ return rows;
293
+ }
294
+
295
+ export async function readCodexProjects(page) {
296
+ const projects = await page.evaluate(`(${collectCodexProjectsFromDocument.toString()})()`);
297
+ if (!Array.isArray(projects)) {
298
+ throw new CommandExecutionError('Codex sidebar project extraction returned an invalid payload');
299
+ }
300
+ return projects;
301
+ }
302
+
303
+ export async function openCodexConversation(page, kwargs) {
304
+ if (!hasConversationTarget(kwargs))
305
+ return null;
306
+ const index = parseOptionalPositiveIntegerOption(kwargs.index, 'codex conversation --index');
307
+ const threadId = kwargs['thread-id'] ? requireNonEmptyOption(kwargs['thread-id'], 'codex conversation --thread-id') : '';
308
+ const target = {
309
+ project: kwargs.project || '',
310
+ conversation: kwargs.conversation || '',
311
+ index: index == null ? '' : String(index),
312
+ threadId,
313
+ preferNativeClick: typeof page.nativeClick === 'function',
314
+ };
315
+ let result = await page.evaluate(`(${selectCodexConversationInDocument.toString()})(${JSON.stringify(target)})`);
316
+ if (result?.expanded) {
317
+ if (typeof page.nativeClick === 'function' && Number.isFinite(result.x) && Number.isFinite(result.y)) {
318
+ await page.nativeClick(result.x, result.y);
319
+ }
320
+ await page.wait(0.75);
321
+ result = await page.evaluate(`(${selectCodexConversationInDocument.toString()})(${JSON.stringify(target)})`);
322
+ }
323
+ if (!result?.ok || !result?.selected) {
324
+ const detail = result?.conversations
325
+ ? ` Available: ${result.conversations.map(item => `${item.index}. ${item.title}`).join('; ')}`
326
+ : result?.projects
327
+ ? ` Available projects: ${result.projects.join(', ')}`
328
+ : '';
329
+ const message = `${result?.error || 'Could not select Codex conversation'}${detail}`;
330
+ if (result?.error?.startsWith('Invalid conversation index:')) {
331
+ throw new ArgumentError(message);
332
+ }
333
+ if (result?.error?.startsWith('Multiple conversations matched')) {
334
+ throw new ArgumentError(message, 'Pass --project or --thread-id to disambiguate the target conversation.');
335
+ }
336
+ if (result?.error?.startsWith('Project not found:')
337
+ || result?.error?.startsWith('Conversation not found:')
338
+ || result?.error?.startsWith('Thread not found:')
339
+ || result?.error?.startsWith('No visible conversations under project:')) {
340
+ throw new EmptyResultError('codex conversation', message);
341
+ }
342
+ throw new CommandExecutionError(message, 'Open the Codex sidebar and verify project/conversation rows are visible.');
343
+ }
344
+ if (typeof page.nativeClick === 'function' && Number.isFinite(result.x) && Number.isFinite(result.y)) {
345
+ await page.nativeClick(result.x, result.y);
346
+ }
347
+ await page.wait(1);
348
+ return result;
349
+ }
350
+
351
+ export const conversationSelectionArgs = [
352
+ { name: 'project', required: false, help: 'Project label or path to select before running the command' },
353
+ { name: 'conversation', required: false, help: 'Conversation title to select within --project' },
354
+ { name: 'index', required: false, help: '1-based conversation index within --project' },
355
+ { name: 'thread-id', required: false, help: 'Exact Codex thread id to select' },
356
+ ];
@@ -0,0 +1,329 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
3
+ import { askCommand } from './ask.js';
4
+ import { historyCommand } from './history.js';
5
+ import { projectsCommand } from './projects.js';
6
+ import {
7
+ collectCodexProjectsFromDocument,
8
+ flattenCodexProjects,
9
+ openCodexConversation,
10
+ selectCodexConversationInDocument,
11
+ } from './sidebar.js';
12
+
13
+ class FakeElement {
14
+ constructor(tagName = 'div', attrs = {}, children = [], text = '') {
15
+ this.tagName = tagName.toUpperCase();
16
+ this.attrs = attrs;
17
+ this.children = children;
18
+ this.parentElement = null;
19
+ this.textContent = text;
20
+ this.innerText = text;
21
+ this.className = attrs.class || '';
22
+ this.listeners = new Map();
23
+ for (const child of children) {
24
+ child.parentElement = this;
25
+ }
26
+ }
27
+
28
+ getAttribute(name) {
29
+ return this.attrs[name] ?? null;
30
+ }
31
+
32
+ addEventListener(name, fn) {
33
+ const listeners = this.listeners.get(name) || [];
34
+ listeners.push(fn);
35
+ this.listeners.set(name, listeners);
36
+ }
37
+
38
+ click() {
39
+ for (const listener of this.listeners.get('click') || []) {
40
+ listener();
41
+ }
42
+ }
43
+
44
+ scrollIntoView() {
45
+ }
46
+
47
+ closest(selector) {
48
+ let current = this;
49
+ while (current) {
50
+ if (matchesSelector(current, selector))
51
+ return current;
52
+ current = current.parentElement;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ querySelectorAll(selector) {
58
+ const selectors = selector.split(',').map(part => part.trim());
59
+ const results = [];
60
+ const visit = (node) => {
61
+ if (selectors.some(part => matchesSelector(node, part))) {
62
+ results.push(node);
63
+ }
64
+ for (const child of node.children) {
65
+ visit(child);
66
+ }
67
+ };
68
+ for (const child of this.children) {
69
+ visit(child);
70
+ }
71
+ return results;
72
+ }
73
+ }
74
+
75
+ function matchesSelector(node, selector) {
76
+ if (selector === '[data-app-action-sidebar-project-row]') {
77
+ return node.getAttribute('data-app-action-sidebar-project-row') !== null;
78
+ }
79
+ if (selector === '[data-app-action-sidebar-thread-row]') {
80
+ return node.getAttribute('data-app-action-sidebar-thread-row') !== null;
81
+ }
82
+ if (selector === '[role="listitem"][aria-label]') {
83
+ return node.getAttribute('role') === 'listitem' && node.getAttribute('aria-label') !== null;
84
+ }
85
+ if (selector === '.tabular-nums') {
86
+ return String(node.className || '').split(/\s+/).includes('tabular-nums');
87
+ }
88
+ if (selector === '[class*="tabular-nums"]') {
89
+ return String(node.className || '').includes('tabular-nums');
90
+ }
91
+ if (selector === '[class*="description"]') {
92
+ return String(node.className || '').includes('description');
93
+ }
94
+ return false;
95
+ }
96
+
97
+ function el(tagName, attrs, children = [], text = '') {
98
+ return new FakeElement(tagName, attrs, children, text);
99
+ }
100
+
101
+ function thread(attrs, title, updated) {
102
+ return el('div', {
103
+ role: 'button',
104
+ 'data-app-action-sidebar-thread-row': '',
105
+ 'data-app-action-sidebar-thread-title': title,
106
+ 'data-app-action-sidebar-thread-id': attrs.threadId,
107
+ 'data-app-action-sidebar-thread-host-id': attrs.hostId || '',
108
+ 'data-app-action-sidebar-thread-kind': attrs.kind || '',
109
+ 'data-app-action-sidebar-thread-active': attrs.active ? 'true' : 'false',
110
+ 'data-app-action-sidebar-thread-pinned': attrs.pinned ? 'true' : 'false',
111
+ }, [
112
+ el('span', { 'data-thread-title': '' }, [], title),
113
+ el('span', { class: 'tabular-nums' }, [], updated),
114
+ ], `${title} ${updated}`);
115
+ }
116
+
117
+ function project(label, projectPath, children) {
118
+ return el('div', { role: 'listitem', 'aria-label': label }, [
119
+ el('div', {
120
+ role: 'button',
121
+ 'aria-expanded': 'true',
122
+ 'data-app-action-sidebar-project-row': '',
123
+ 'data-app-action-sidebar-project-label': label,
124
+ 'data-app-action-sidebar-project-id': projectPath,
125
+ }, [], label),
126
+ ...children,
127
+ ]);
128
+ }
129
+
130
+ function fixtureDocument() {
131
+ return el('document', {}, [
132
+ project('stock', '/Users/youngcan/stock', [
133
+ thread({ threadId: 'local:stock-sync', hostId: 'local', kind: 'local', active: true }, '同步各仓库最新代码', '4 小时'),
134
+ thread({ threadId: 'local:trading-agents' }, '借鉴 TradingAgents', '2 小时'),
135
+ ]),
136
+ project('opencli', '/Users/youngcan/opencli', [
137
+ thread({ threadId: 'local:opencli-groups' }, '统一 opencli 二级命令分组', '1 天'),
138
+ ]),
139
+ ]);
140
+ }
141
+
142
+ describe('codex sidebar helpers', () => {
143
+ it('collects projects and visible conversations from Codex data attributes', () => {
144
+ const projects = collectCodexProjectsFromDocument(fixtureDocument());
145
+
146
+ expect(projects).toHaveLength(2);
147
+ expect(projects[0]).toMatchObject({
148
+ project: 'stock',
149
+ projectPath: '/Users/youngcan/stock',
150
+ collapsed: false,
151
+ });
152
+ expect(projects[0].conversations[0]).toMatchObject({
153
+ index: 1,
154
+ title: '同步各仓库最新代码',
155
+ updated: '4 小时',
156
+ active: true,
157
+ threadId: 'local:stock-sync',
158
+ });
159
+ });
160
+
161
+ it('flattens project rows with project filters', () => {
162
+ const projects = collectCodexProjectsFromDocument(fixtureDocument());
163
+ const rows = flattenCodexProjects(projects, { project: 'opencli' });
164
+
165
+ expect(rows).toEqual([
166
+ expect.objectContaining({
167
+ Project: 'opencli',
168
+ Index: 1,
169
+ Title: '统一 opencli 二级命令分组',
170
+ Updated: '1 天',
171
+ }),
172
+ ]);
173
+ });
174
+
175
+ it('rejects invalid project/history limits instead of silently ignoring them', () => {
176
+ const projects = collectCodexProjectsFromDocument(fixtureDocument());
177
+
178
+ expect(() => flattenCodexProjects(projects, { limit: '0' })).toThrowError(ArgumentError);
179
+ expect(() => flattenCodexProjects(projects, { limit: '1.5' })).toThrowError(ArgumentError);
180
+ expect(() => flattenCodexProjects(projects, { limit: 'abc' })).toThrowError(ArgumentError);
181
+ });
182
+
183
+ it('does not match nested project paths when filtering by a parent label', () => {
184
+ const projects = collectCodexProjectsFromDocument(fixtureDocument());
185
+ projects.push({
186
+ index: 3,
187
+ project: 'nested',
188
+ projectPath: '/Users/youngcan/opencli/nested',
189
+ collapsed: false,
190
+ conversations: [
191
+ { index: 1, title: 'Nested thread', updated: '', active: false, threadId: 'local:nested' },
192
+ ],
193
+ });
194
+
195
+ const rows = flattenCodexProjects(projects, { project: 'opencli' });
196
+
197
+ expect(rows.map(row => row.Project)).toEqual(['opencli']);
198
+ });
199
+
200
+ it('selects a conversation by project and title', () => {
201
+ const doc = fixtureDocument();
202
+ const selected = [];
203
+ for (const row of doc.querySelectorAll('[data-app-action-sidebar-thread-row]')) {
204
+ row.addEventListener('click', () => selected.push(row.getAttribute('data-app-action-sidebar-thread-id')));
205
+ }
206
+
207
+ const result = selectCodexConversationInDocument({
208
+ project: 'stock',
209
+ conversation: 'TradingAgents',
210
+ }, doc);
211
+
212
+ expect(result).toMatchObject({
213
+ ok: true,
214
+ selected: true,
215
+ project: 'stock',
216
+ conversation: '借鉴 TradingAgents',
217
+ threadId: 'local:trading-agents',
218
+ index: 2,
219
+ });
220
+ expect(selected).toEqual(['local:trading-agents']);
221
+ });
222
+
223
+ it('does not dispatch DOM click when native click is preferred', () => {
224
+ const doc = fixtureDocument();
225
+ const selected = [];
226
+ for (const row of doc.querySelectorAll('[data-app-action-sidebar-thread-row]')) {
227
+ row.addEventListener('click', () => selected.push(row.getAttribute('data-app-action-sidebar-thread-id')));
228
+ }
229
+
230
+ const result = selectCodexConversationInDocument({
231
+ project: 'stock',
232
+ conversation: 'TradingAgents',
233
+ preferNativeClick: true,
234
+ }, doc);
235
+
236
+ expect(result).toMatchObject({
237
+ ok: true,
238
+ selected: true,
239
+ threadId: 'local:trading-agents',
240
+ });
241
+ expect(selected).toEqual([]);
242
+ });
243
+
244
+ it('selects a conversation by index within a project', () => {
245
+ const result = selectCodexConversationInDocument({
246
+ project: '/Users/youngcan/opencli',
247
+ index: '1',
248
+ }, fixtureDocument());
249
+
250
+ expect(result).toMatchObject({
251
+ ok: true,
252
+ project: 'opencli',
253
+ conversation: '统一 opencli 二级命令分组',
254
+ threadId: 'local:opencli-groups',
255
+ });
256
+ });
257
+
258
+ it('reports exact thread-id misses as not found', () => {
259
+ const result = selectCodexConversationInDocument({
260
+ project: 'stock',
261
+ threadId: 'local:missing',
262
+ }, fixtureDocument());
263
+
264
+ expect(result).toMatchObject({
265
+ ok: false,
266
+ error: 'Thread not found: local:missing',
267
+ });
268
+ });
269
+
270
+ it('maps project/conversation misses to EmptyResultError in command selection', async () => {
271
+ const page = {
272
+ evaluate: async () => selectCodexConversationInDocument({ project: 'missing' }, fixtureDocument()),
273
+ };
274
+
275
+ await expect(openCodexConversation(page, { project: 'missing' })).rejects.toBeInstanceOf(EmptyResultError);
276
+ });
277
+
278
+ it('maps missing sidebar DOM to CommandExecutionError instead of empty success', async () => {
279
+ const page = {
280
+ evaluate: async () => selectCodexConversationInDocument({ conversation: 'anything' }, el('document', {}, [])),
281
+ };
282
+
283
+ await expect(openCodexConversation(page, { conversation: 'anything' })).rejects.toBeInstanceOf(CommandExecutionError);
284
+ });
285
+
286
+ it('maps ambiguous conversation selection to ArgumentError', async () => {
287
+ const duplicateDoc = el('document', {}, [
288
+ project('alpha', '/tmp/alpha', [
289
+ thread({ threadId: 'local:one' }, 'Shared title', '1 小时'),
290
+ ]),
291
+ project('beta', '/tmp/beta', [
292
+ thread({ threadId: 'local:two' }, 'Shared title', '2 小时'),
293
+ ]),
294
+ ]);
295
+ const page = {
296
+ evaluate: async () => selectCodexConversationInDocument({ conversation: 'Shared title' }, duplicateDoc),
297
+ };
298
+
299
+ await expect(openCodexConversation(page, { conversation: 'Shared title' })).rejects.toBeInstanceOf(ArgumentError);
300
+ });
301
+ });
302
+
303
+ describe('codex sidebar commands', () => {
304
+ it('projects fails empty result instead of returning a sentinel row', async () => {
305
+ const page = {
306
+ evaluate: async () => [],
307
+ };
308
+
309
+ await expect(projectsCommand.func(page, {})).rejects.toBeInstanceOf(EmptyResultError);
310
+ });
311
+
312
+ it('history fails empty result instead of returning a sentinel row', async () => {
313
+ const page = {
314
+ evaluate: async () => [],
315
+ };
316
+
317
+ await expect(historyCommand.func(page, {})).rejects.toBeInstanceOf(EmptyResultError);
318
+ });
319
+
320
+ it('ask rejects invalid timeout instead of falling back to the default', async () => {
321
+ const page = {
322
+ evaluate: async () => 0,
323
+ };
324
+
325
+ await expect(askCommand.func(page, { text: 'hello', timeout: 'bogus' })).rejects.toBeInstanceOf(ArgumentError);
326
+ await expect(askCommand.func(page, { text: 'hello', timeout: '0' })).rejects.toBeInstanceOf(ArgumentError);
327
+ await expect(askCommand.func(page, { text: 'hello', timeout: '1.5' })).rejects.toBeInstanceOf(ArgumentError);
328
+ });
329
+ });