@huseynsnmz/pi-suite 0.0.0-stage → 0.2.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 (274) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +88 -2
  4. package/SECURITY.md +5 -0
  5. package/package.json +112 -4
  6. package/packages/board/CHANGELOG.md +340 -0
  7. package/packages/board/CONTRIBUTING.md +7 -0
  8. package/packages/board/LICENSE +21 -0
  9. package/packages/board/README.md +88 -0
  10. package/packages/board/SECURITY.md +19 -0
  11. package/packages/board/docs/how-it-works.md +31 -0
  12. package/packages/board/docs/reference.md +278 -0
  13. package/packages/board/package.json +81 -0
  14. package/packages/board/runner/job-runner-legacy.mjs +68 -0
  15. package/packages/board/runner/job-runner.mjs +834 -0
  16. package/packages/board/runner/pty-runner-legacy.mjs +50 -0
  17. package/packages/board/runner/pty-runner.mjs +2175 -0
  18. package/packages/board/runner/state-coordinator.mjs +673 -0
  19. package/packages/board/runner/state-runner.mjs +203 -0
  20. package/packages/board/runner/title-runner.mjs +80 -0
  21. package/packages/board/src/commands/agent-board.ts +394 -0
  22. package/packages/board/src/commands/attach-decision.mjs +66 -0
  23. package/packages/board/src/commands/attach-flow.ts +251 -0
  24. package/packages/board/src/commands/bg.ts +103 -0
  25. package/packages/board/src/commands/board-model.ts +21 -0
  26. package/packages/board/src/core/atomic.mjs +184 -0
  27. package/packages/board/src/core/attach-delete.mjs +24 -0
  28. package/packages/board/src/core/attach-frame-proof.mjs +870 -0
  29. package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
  30. package/packages/board/src/core/attach-frame-stream.mjs +266 -0
  31. package/packages/board/src/core/attach-view-proof.mjs +709 -0
  32. package/packages/board/src/core/auto-state.mjs +377 -0
  33. package/packages/board/src/core/background-runs.mjs +273 -0
  34. package/packages/board/src/core/code-refs-store.mjs +315 -0
  35. package/packages/board/src/core/code-refs.mjs +913 -0
  36. package/packages/board/src/core/config.mjs +77 -0
  37. package/packages/board/src/core/control-protocol.mjs +482 -0
  38. package/packages/board/src/core/coordinator-client.mjs +633 -0
  39. package/packages/board/src/core/coordinator-journal.mjs +282 -0
  40. package/packages/board/src/core/coordinator-protocol.mjs +14 -0
  41. package/packages/board/src/core/cwd-stats.mjs +114 -0
  42. package/packages/board/src/core/dashboard-render.mjs +10 -0
  43. package/packages/board/src/core/derive.mjs +117 -0
  44. package/packages/board/src/core/diagnostics.mjs +133 -0
  45. package/packages/board/src/core/double-press.mjs +26 -0
  46. package/packages/board/src/core/editor-state-reporter.mjs +112 -0
  47. package/packages/board/src/core/events.mjs +286 -0
  48. package/packages/board/src/core/evidence.mjs +245 -0
  49. package/packages/board/src/core/follow-up-queue.mjs +211 -0
  50. package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
  51. package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
  52. package/packages/board/src/core/frame-witness-update.mjs +35 -0
  53. package/packages/board/src/core/heuristics.mjs +313 -0
  54. package/packages/board/src/core/hold-input.mjs +27 -0
  55. package/packages/board/src/core/host-coordination.mjs +300 -0
  56. package/packages/board/src/core/host-crash.mjs +79 -0
  57. package/packages/board/src/core/host-probe.mjs +207 -0
  58. package/packages/board/src/core/host-protocol.mjs +73 -0
  59. package/packages/board/src/core/ids.mjs +35 -0
  60. package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
  61. package/packages/board/src/core/invocation.mjs +43 -0
  62. package/packages/board/src/core/launch-options.mjs +418 -0
  63. package/packages/board/src/core/launch.mjs +157 -0
  64. package/packages/board/src/core/load-config.mjs +4 -0
  65. package/packages/board/src/core/locks.mjs +530 -0
  66. package/packages/board/src/core/paths.mjs +183 -0
  67. package/packages/board/src/core/pid.mjs +75 -0
  68. package/packages/board/src/core/prewarm-schedule.mjs +41 -0
  69. package/packages/board/src/core/process-identity-probe.mjs +228 -0
  70. package/packages/board/src/core/prompt-transport.mjs +13 -0
  71. package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
  72. package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
  73. package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
  74. package/packages/board/src/core/pty-attach-render.mjs +229 -0
  75. package/packages/board/src/core/pty-links.mjs +81 -0
  76. package/packages/board/src/core/pty-scroll.mjs +522 -0
  77. package/packages/board/src/core/pty-support.mjs +327 -0
  78. package/packages/board/src/core/repo.mjs +103 -0
  79. package/packages/board/src/core/rows.mjs +414 -0
  80. package/packages/board/src/core/screen-log-gc.mjs +198 -0
  81. package/packages/board/src/core/screen-log.mjs +160 -0
  82. package/packages/board/src/core/session-view.mjs +174 -0
  83. package/packages/board/src/core/slash-commands.mjs +52 -0
  84. package/packages/board/src/core/state-commands.mjs +757 -0
  85. package/packages/board/src/core/status-consistency.mjs +98 -0
  86. package/packages/board/src/core/steering-prompts.mjs +34 -0
  87. package/packages/board/src/core/steering.mjs +133 -0
  88. package/packages/board/src/core/stop-latch.mjs +41 -0
  89. package/packages/board/src/core/store.mjs +523 -0
  90. package/packages/board/src/core/suspend.mjs +36 -0
  91. package/packages/board/src/core/terminal-attach-client.mjs +981 -0
  92. package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
  93. package/packages/board/src/core/terminal-model.mjs +222 -0
  94. package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
  95. package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
  96. package/packages/board/src/core/terminal-snapshot.mjs +440 -0
  97. package/packages/board/src/core/test-knobs.mjs +28 -0
  98. package/packages/board/src/core/title.mjs +43 -0
  99. package/packages/board/src/core/types.mjs +414 -0
  100. package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
  101. package/packages/board/src/core/worktree.mjs +65 -0
  102. package/packages/board/src/index.ts +303 -0
  103. package/packages/board/src/runtime/service.mjs +3222 -0
  104. package/packages/board/src/ui/chrome.ts +20 -0
  105. package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
  106. package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
  107. package/packages/board/src/ui/dashboard.ts +2237 -0
  108. package/packages/board/src/ui/double-left.ts +30 -0
  109. package/packages/board/src/ui/external-editor.ts +25 -0
  110. package/packages/board/src/ui/focus.ts +32 -0
  111. package/packages/board/src/ui/pty-attach.ts +3184 -0
  112. package/packages/board/src/ui/waiting.ts +30 -0
  113. package/packages/commands/LICENSE +21 -0
  114. package/packages/commands/README.md +19 -0
  115. package/packages/commands/package.json +22 -0
  116. package/packages/commands/src/index.ts +43 -0
  117. package/packages/commands/src/security-review.ts +109 -0
  118. package/packages/frame/CHANGELOG.md +113 -0
  119. package/packages/frame/CONTRIBUTING.md +7 -0
  120. package/packages/frame/LICENSE +22 -0
  121. package/packages/frame/README.md +105 -0
  122. package/packages/frame/SECURITY.md +16 -0
  123. package/packages/frame/docs/how-it-works.md +53 -0
  124. package/packages/frame/docs/reference.md +78 -0
  125. package/packages/frame/package.json +72 -0
  126. package/packages/frame/src/data/git.ts +90 -0
  127. package/packages/frame/src/data/github.ts +140 -0
  128. package/packages/frame/src/data/gitlab.ts +63 -0
  129. package/packages/frame/src/data/mode.ts +49 -0
  130. package/packages/frame/src/data/past-prompts.ts +57 -0
  131. package/packages/frame/src/data/runner.ts +32 -0
  132. package/packages/frame/src/data/snapshot.ts +223 -0
  133. package/packages/frame/src/data/start-notes.ts +104 -0
  134. package/packages/frame/src/data/telemetry.ts +337 -0
  135. package/packages/frame/src/data/todos.ts +162 -0
  136. package/packages/frame/src/data/totals.ts +94 -0
  137. package/packages/frame/src/data/visits.ts +45 -0
  138. package/packages/frame/src/index.ts +696 -0
  139. package/packages/frame/src/kit/LICENSE +21 -0
  140. package/packages/frame/src/kit/app/attribution.ts +30 -0
  141. package/packages/frame/src/kit/app/builder.ts +186 -0
  142. package/packages/frame/src/kit/app/claim.ts +27 -0
  143. package/packages/frame/src/kit/app/feature.ts +91 -0
  144. package/packages/frame/src/kit/app/host.ts +214 -0
  145. package/packages/frame/src/kit/app/renderers.ts +62 -0
  146. package/packages/frame/src/kit/app/scope.ts +125 -0
  147. package/packages/frame/src/kit/board/model.ts +337 -0
  148. package/packages/frame/src/kit/contract.ts +79 -0
  149. package/packages/frame/src/kit/contracts/accounts.ts +33 -0
  150. package/packages/frame/src/kit/contracts/ask.ts +120 -0
  151. package/packages/frame/src/kit/contracts/board.ts +6 -0
  152. package/packages/frame/src/kit/contracts/by.ts +22 -0
  153. package/packages/frame/src/kit/contracts/calls.ts +46 -0
  154. package/packages/frame/src/kit/contracts/events.ts +89 -0
  155. package/packages/frame/src/kit/contracts/mode.ts +26 -0
  156. package/packages/frame/src/kit/contracts/names.ts +12 -0
  157. package/packages/frame/src/kit/contracts/peers.ts +77 -0
  158. package/packages/frame/src/kit/contracts/prompts.ts +8 -0
  159. package/packages/frame/src/kit/contracts/screen.ts +75 -0
  160. package/packages/frame/src/kit/contracts/skills.ts +81 -0
  161. package/packages/frame/src/kit/contracts/sources.ts +81 -0
  162. package/packages/frame/src/kit/contracts/views.ts +42 -0
  163. package/packages/frame/src/kit/control.ts +59 -0
  164. package/packages/frame/src/kit/dark-theme.ts +41 -0
  165. package/packages/frame/src/kit/decode.ts +210 -0
  166. package/packages/frame/src/kit/events/log.ts +313 -0
  167. package/packages/frame/src/kit/format.ts +25 -0
  168. package/packages/frame/src/kit/host.ts +4 -0
  169. package/packages/frame/src/kit/index.ts +25 -0
  170. package/packages/frame/src/kit/legacy-tasks.ts +39 -0
  171. package/packages/frame/src/kit/overlay-images.ts +36 -0
  172. package/packages/frame/src/kit/paths.ts +16 -0
  173. package/packages/frame/src/kit/screen/client.ts +74 -0
  174. package/packages/frame/src/kit/screen/feature.ts +64 -0
  175. package/packages/frame/src/kit/screen/model.ts +182 -0
  176. package/packages/frame/src/kit/screen/view.ts +838 -0
  177. package/packages/frame/src/kit/settings/files.ts +90 -0
  178. package/packages/frame/src/kit/settings/setting.ts +57 -0
  179. package/packages/frame/src/kit/settings/store.ts +192 -0
  180. package/packages/frame/src/kit/sources/file.ts +129 -0
  181. package/packages/frame/src/kit/sources/follow.ts +155 -0
  182. package/packages/frame/src/kit/testing.ts +260 -0
  183. package/packages/frame/src/kit/totals.ts +72 -0
  184. package/packages/frame/src/kit/ui/entry.ts +75 -0
  185. package/packages/frame/src/kit/ui/frame.ts +212 -0
  186. package/packages/frame/src/kit/ui/glyphs.ts +132 -0
  187. package/packages/frame/src/kit/ui/scroll.ts +14 -0
  188. package/packages/frame/src/render/diff.ts +157 -0
  189. package/packages/frame/src/render/fit.ts +86 -0
  190. package/packages/frame/src/render/format.ts +108 -0
  191. package/packages/frame/src/render/frame.ts +106 -0
  192. package/packages/frame/src/render/glyphs.ts +189 -0
  193. package/packages/frame/src/render/keys.ts +43 -0
  194. package/packages/frame/src/render/lead.ts +37 -0
  195. package/packages/frame/src/render/paint.ts +135 -0
  196. package/packages/frame/src/render/safe.ts +19 -0
  197. package/packages/frame/src/render/segments.ts +735 -0
  198. package/packages/frame/src/render/streaming.ts +223 -0
  199. package/packages/frame/src/render/timing.ts +106 -0
  200. package/packages/frame/src/render/tools.ts +167 -0
  201. package/packages/frame/src/settings.ts +105 -0
  202. package/packages/frame/src/ui/editor.ts +272 -0
  203. package/packages/frame/src/ui/effort.ts +36 -0
  204. package/packages/frame/src/ui/footer.ts +165 -0
  205. package/packages/frame/src/ui/header.ts +362 -0
  206. package/packages/frame/src/ui/history-search.ts +23 -0
  207. package/packages/frame/src/ui/interrupt.ts +21 -0
  208. package/packages/frame/src/ui/outputs.ts +170 -0
  209. package/packages/frame/src/ui/pi-entries.ts +298 -0
  210. package/packages/frame/src/ui/previews.ts +72 -0
  211. package/packages/frame/src/ui/queue.ts +75 -0
  212. package/packages/frame/src/ui/screen.ts +59 -0
  213. package/packages/frame/src/ui/timings.ts +48 -0
  214. package/packages/frame/src/ui/todos.ts +205 -0
  215. package/packages/frame/src/ui/tool-frame.ts +595 -0
  216. package/packages/frame/src/ui/working.ts +25 -0
  217. package/packages/frame/src/view/state.ts +97 -0
  218. package/packages/frame/src/view/transcript.ts +175 -0
  219. package/packages/frame/src/view/view.ts +162 -0
  220. package/packages/memory/CONTRIBUTING.md +7 -0
  221. package/packages/memory/LICENSE +21 -0
  222. package/packages/memory/README.md +44 -0
  223. package/packages/memory/SECURITY.md +20 -0
  224. package/packages/memory/package.json +55 -0
  225. package/packages/memory/skills/review-memory/SKILL.md +48 -0
  226. package/packages/memory/src/index.ts +163 -0
  227. package/packages/memory/src/prompt.ts +25 -0
  228. package/packages/memory/src/review.ts +150 -0
  229. package/packages/memory/src/secrets.ts +22 -0
  230. package/packages/memory/src/store.ts +173 -0
  231. package/packages/modes/CHANGELOG.md +104 -0
  232. package/packages/modes/CONTRIBUTING.md +7 -0
  233. package/packages/modes/LICENSE +21 -0
  234. package/packages/modes/README.md +174 -0
  235. package/packages/modes/SECURITY.md +17 -0
  236. package/packages/modes/package.json +55 -0
  237. package/packages/modes/skills/review-permissions/SKILL.md +54 -0
  238. package/packages/modes/src/bash.ts +218 -0
  239. package/packages/modes/src/deny-off.ts +62 -0
  240. package/packages/modes/src/git-safety.ts +72 -0
  241. package/packages/modes/src/index.ts +338 -0
  242. package/packages/modes/src/interview.ts +206 -0
  243. package/packages/modes/src/judge-gate.ts +113 -0
  244. package/packages/modes/src/modes.ts +121 -0
  245. package/packages/modes/src/review/apply.mjs +156 -0
  246. package/packages/modes/src/review/collect.mjs +208 -0
  247. package/packages/modes/src/review/tools.ts +71 -0
  248. package/packages/modes/src/sandbox.ts +37 -0
  249. package/packages/router/CHANGELOG.md +8 -0
  250. package/packages/router/LICENSE +21 -0
  251. package/packages/router/README.md +90 -0
  252. package/packages/router/package.json +51 -0
  253. package/packages/router/src/config.ts +65 -0
  254. package/packages/router/src/index.ts +172 -0
  255. package/packages/router/src/judge.ts +51 -0
  256. package/packages/router/src/route.ts +67 -0
  257. package/packages/web/CHANGELOG.md +36 -0
  258. package/packages/web/LICENSE +21 -0
  259. package/packages/web/README.md +87 -0
  260. package/packages/web/SECURITY.md +13 -0
  261. package/packages/web/package.json +59 -0
  262. package/packages/web/src/config.ts +109 -0
  263. package/packages/web/src/dnsfilter.ts +97 -0
  264. package/packages/web/src/extract.ts +65 -0
  265. package/packages/web/src/index.ts +217 -0
  266. package/packages/web/src/markdown.ts +122 -0
  267. package/packages/web/src/net.ts +109 -0
  268. package/packages/web/src/reader.ts +73 -0
  269. package/packages/web/src/sanitize.ts +50 -0
  270. package/packages/web/src/search.ts +76 -0
  271. package/packages/web/src/secrets.ts +22 -0
  272. package/packages/web/src/validate.ts +186 -0
  273. package/src/index.ts +156 -0
  274. package/src/settings.ts +79 -0
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Launch dialog helpers: resolve cwd suggestions, scoped model choices, and thinking options.
3
+ */
4
+ import { existsSync, readdirSync, statSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { SettingsManager } from "@earendil-works/pi-coding-agent";
7
+
8
+ /** @typedef {"off"|"minimal"|"low"|"medium"|"high"|"xhigh"|"max"} ThinkingLevel */
9
+
10
+ /**
11
+ * @typedef {Object} LaunchModelLike
12
+ * @property {string} provider
13
+ * @property {string} id
14
+ * @property {string=} name
15
+ * @property {boolean=} reasoning
16
+ * @property {Partial<Record<ThinkingLevel, string|null>>=} thinkingLevelMap
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} LaunchModelChoice
21
+ * @property {LaunchModelLike} model
22
+ * @property {ThinkingLevel=} thinkingLevel
23
+ */
24
+
25
+ /**
26
+ * @typedef {Object} LaunchContext
27
+ * @property {LaunchModelChoice[]} choices
28
+ * @property {LaunchModelLike|null} selectedModel
29
+ * @property {ThinkingLevel} thinking
30
+ * @property {ThinkingLevel[]} thinkingOptions
31
+ * @property {"scoped"|"all"} scopeSource
32
+ */
33
+
34
+ export const THINKING_LEVELS = /** @type {const} */ (["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
35
+
36
+ /** @param {unknown} value @returns {value is ThinkingLevel} */
37
+ export function isThinkingLevel(value) {
38
+ return typeof value === "string" && THINKING_LEVELS.includes(/** @type {ThinkingLevel} */ (value));
39
+ }
40
+
41
+ /** @param {LaunchModelLike|null|undefined} model */
42
+ export function canonicalModelRef(model) {
43
+ return model ? `${model.provider}/${model.id}` : "";
44
+ }
45
+
46
+ /**
47
+ * Whether a stored model reference resolves to a currently-available model.
48
+ * Same rule as the dashboard launch picker: case-insensitive exact "provider/id".
49
+ * A null/empty reference is trivially available (no constraint), and an
50
+ * empty/unavailable model list must not block launches (the caller cannot judge
51
+ * availability, so the conservative answer is allow — issue #90).
52
+ * @param {string|null|undefined} modelRef
53
+ * @param {LaunchModelLike[]|undefined|null} availableModels
54
+ * @returns {boolean}
55
+ */
56
+ export function modelRefAvailable(modelRef, availableModels) {
57
+ const ref = String(modelRef ?? "").trim().toLowerCase();
58
+ if (!ref) return true;
59
+ if (!availableModels || availableModels.length === 0) return true;
60
+ return availableModels.some((model) => `${model.provider}/${model.id}`.toLowerCase() === ref);
61
+ }
62
+
63
+ /** @param {LaunchModelLike|null|undefined} a @param {LaunchModelLike|null|undefined} b */
64
+ export function sameModel(a, b) {
65
+ return Boolean(a && b && a.provider === b.provider && a.id === b.id);
66
+ }
67
+
68
+ /** @param {LaunchModelLike|null|undefined} model @returns {ThinkingLevel[]} */
69
+ export function supportedThinkingLevels(model) {
70
+ if (!model?.reasoning) return ["off"];
71
+ const map = model.thinkingLevelMap ?? {};
72
+ const levels = THINKING_LEVELS.filter((level) => map[level] !== null);
73
+ return levels.length ? levels : [...THINKING_LEVELS];
74
+ }
75
+
76
+ /**
77
+ * @param {LaunchModelLike|null|undefined} model
78
+ * @param {ThinkingLevel|undefined|null} requested
79
+ * @param {ThinkingLevel=} fallback
80
+ * @returns {ThinkingLevel}
81
+ */
82
+ export function clampThinkingLevel(model, requested, fallback = "off") {
83
+ const supported = supportedThinkingLevels(model);
84
+ if (requested && supported.includes(requested)) return requested;
85
+ if (supported.includes(fallback)) return fallback;
86
+ return supported[0] ?? "off";
87
+ }
88
+
89
+ /**
90
+ * Resolve the model choices for a cwd using pi's scoped-model settings semantics.
91
+ * Falls back to all available models when no scoped models are configured or none resolve.
92
+ *
93
+ * @param {string} cwd
94
+ * @param {LaunchModelLike[]} availableModels
95
+ * @param {LaunchModelLike|null|undefined} preferredModel
96
+ * @param {ThinkingLevel|undefined|null} preferredThinking
97
+ * @returns {LaunchContext}
98
+ */
99
+ export function resolveLaunchContext(cwd, availableModels, preferredModel, preferredThinking) {
100
+ const settings = SettingsManager.create(cwd);
101
+ const patterns = settings.getEnabledModels();
102
+ const scoped = resolveScopedModels(patterns, availableModels);
103
+ const fallbackChoices = sortModels(availableModels, preferredModel).map((model) => ({ model }));
104
+ const choices = scoped.length ? scoped : fallbackChoices;
105
+ const scopeSource = scoped.length ? "scoped" : "all";
106
+ const defaultThinking = normalizeThinking(settings.getDefaultThinkingLevel()) ?? preferredThinking ?? "off";
107
+ const selectedChoice = preferredModel ? choices.find((choice) => sameModel(choice.model, preferredModel)) : undefined;
108
+ const selectedModel = selectedChoice?.model ?? choices[0]?.model ?? preferredModel ?? null;
109
+ const thinkingBase = selectedChoice?.thinkingLevel ?? preferredThinking ?? defaultThinking;
110
+ const thinking = clampThinkingLevel(selectedModel, thinkingBase, defaultThinking);
111
+ return {
112
+ choices,
113
+ selectedModel,
114
+ thinking,
115
+ thinkingOptions: supportedThinkingLevels(selectedModel),
116
+ scopeSource,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * @param {string|undefined} value
122
+ * @returns {ThinkingLevel|undefined}
123
+ */
124
+ function normalizeThinking(value) {
125
+ return isThinkingLevel(value) ? value : undefined;
126
+ }
127
+
128
+ /** @param {LaunchModelLike[]} models @param {LaunchModelLike|null|undefined} current */
129
+ function sortModels(models, current) {
130
+ return [...models].sort((a, b) => {
131
+ const aCurrent = sameModel(a, current);
132
+ const bCurrent = sameModel(b, current);
133
+ if (aCurrent && !bCurrent) return -1;
134
+ if (!aCurrent && bCurrent) return 1;
135
+ const providerCmp = a.provider.localeCompare(b.provider);
136
+ return providerCmp !== 0 ? providerCmp : a.id.localeCompare(b.id);
137
+ });
138
+ }
139
+
140
+ /** @param {string|undefined} patterns @param {LaunchModelLike[]} _available */
141
+ function resolveScopedModels(patterns, _available) {
142
+ const available = Array.isArray(_available) ? _available : [];
143
+ if (!patterns?.length) return [];
144
+ /** @type {LaunchModelChoice[]} */
145
+ const scoped = [];
146
+ for (const rawPattern of patterns) {
147
+ const pattern = String(rawPattern || "").trim();
148
+ if (!pattern) continue;
149
+ if (hasGlob(pattern)) {
150
+ const colonIdx = pattern.lastIndexOf(":");
151
+ let globPattern = pattern;
152
+ /** @type {ThinkingLevel|undefined} */
153
+ let thinkingLevel;
154
+ if (colonIdx !== -1) {
155
+ const suffix = pattern.slice(colonIdx + 1);
156
+ if (isThinkingLevel(suffix)) {
157
+ thinkingLevel = suffix;
158
+ globPattern = pattern.slice(0, colonIdx);
159
+ }
160
+ }
161
+ const matches = available.filter((model) => matchGlob(`${model.provider}/${model.id}`, globPattern) || matchGlob(model.id, globPattern));
162
+ for (const model of matches) pushUnique(scoped, { model, thinkingLevel });
163
+ continue;
164
+ }
165
+ const parsed = parseModelPattern(pattern, available);
166
+ if (parsed.model) pushUnique(scoped, { model: parsed.model, thinkingLevel: parsed.thinkingLevel });
167
+ }
168
+ return scoped;
169
+ }
170
+
171
+ /** @param {LaunchModelChoice[]} choices @param {LaunchModelChoice} choice */
172
+ function pushUnique(choices, choice) {
173
+ if (!choices.find((entry) => sameModel(entry.model, choice.model))) choices.push(choice);
174
+ }
175
+
176
+ /** @param {string} pattern */
177
+ function hasGlob(pattern) {
178
+ return /[*?[]/.test(pattern);
179
+ }
180
+
181
+ /** @param {string} text @param {string} glob */
182
+ function matchGlob(text, glob) {
183
+ try {
184
+ return new RegExp(`^${globToRegExp(glob)}$`, "i").test(text);
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ /** @param {string} glob */
191
+ function globToRegExp(glob) {
192
+ let out = "";
193
+ for (let i = 0; i < glob.length; i++) {
194
+ const ch = glob[i];
195
+ if (ch === "*") {
196
+ out += ".*";
197
+ continue;
198
+ }
199
+ if (ch === "?") {
200
+ out += ".";
201
+ continue;
202
+ }
203
+ if (ch === "[") {
204
+ const end = glob.indexOf("]", i + 1);
205
+ if (end > i) {
206
+ out += glob.slice(i, end + 1);
207
+ i = end;
208
+ continue;
209
+ }
210
+ }
211
+ out += escapeRegExp(ch);
212
+ }
213
+ return out;
214
+ }
215
+
216
+ /** @param {string} text */
217
+ function escapeRegExp(text) {
218
+ return text.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
219
+ }
220
+
221
+ /** @param {string} id */
222
+ function isAlias(id) {
223
+ if (id.endsWith("-latest")) return true;
224
+ return !/-\d{8}$/.test(id);
225
+ }
226
+
227
+ /** @param {string} modelReference @param {LaunchModelLike[]} availableModels */
228
+ function findExactModelReferenceMatch(modelReference, availableModels) {
229
+ const trimmed = modelReference.trim();
230
+ if (!trimmed) return undefined;
231
+ const lower = trimmed.toLowerCase();
232
+ const canonicalMatches = availableModels.filter((model) => `${model.provider}/${model.id}`.toLowerCase() === lower);
233
+ if (canonicalMatches.length === 1) return canonicalMatches[0];
234
+ if (canonicalMatches.length > 1) return undefined;
235
+ const slashIndex = trimmed.indexOf("/");
236
+ if (slashIndex !== -1) {
237
+ const provider = trimmed.slice(0, slashIndex).trim();
238
+ const modelId = trimmed.slice(slashIndex + 1).trim();
239
+ if (provider && modelId) {
240
+ const providerMatches = availableModels.filter((model) => model.provider.toLowerCase() === provider.toLowerCase() && model.id.toLowerCase() === modelId.toLowerCase());
241
+ if (providerMatches.length === 1) return providerMatches[0];
242
+ if (providerMatches.length > 1) return undefined;
243
+ }
244
+ }
245
+ const idMatches = availableModels.filter((model) => model.id.toLowerCase() === lower);
246
+ return idMatches.length === 1 ? idMatches[0] : undefined;
247
+ }
248
+
249
+ /** @param {string} modelPattern @param {LaunchModelLike[]} availableModels */
250
+ function tryMatchModel(modelPattern, availableModels) {
251
+ const exact = findExactModelReferenceMatch(modelPattern, availableModels);
252
+ if (exact) return exact;
253
+ const lower = modelPattern.toLowerCase();
254
+ const matches = availableModels.filter((model) => model.id.toLowerCase().includes(lower) || model.name?.toLowerCase().includes(lower));
255
+ if (!matches.length) return undefined;
256
+ const aliases = matches.filter((model) => isAlias(model.id));
257
+ const dated = matches.filter((model) => !isAlias(model.id));
258
+ if (aliases.length) return aliases.sort((a, b) => b.id.localeCompare(a.id))[0];
259
+ return dated.sort((a, b) => b.id.localeCompare(a.id))[0];
260
+ }
261
+
262
+ /** @param {string} pattern @param {LaunchModelLike[]} availableModels */
263
+ function parseModelPattern(pattern, availableModels) {
264
+ const exact = tryMatchModel(pattern, availableModels);
265
+ if (exact) return { model: exact, thinkingLevel: undefined };
266
+ const lastColon = pattern.lastIndexOf(":");
267
+ if (lastColon === -1) return { model: undefined, thinkingLevel: undefined };
268
+ const prefix = pattern.slice(0, lastColon);
269
+ const suffix = pattern.slice(lastColon + 1);
270
+ if (isThinkingLevel(suffix)) {
271
+ const result = parseModelPattern(prefix, availableModels);
272
+ if (result.model) return { model: result.model, thinkingLevel: suffix };
273
+ return result;
274
+ }
275
+ return parseModelPattern(prefix, availableModels);
276
+ }
277
+
278
+ /** @param {string} input */
279
+ function expandHome(input) {
280
+ const home = process.env.HOME || process.env.USERPROFILE || "";
281
+ if (!home) return input;
282
+ if (input === "~") return home;
283
+ if (input.startsWith(`~${path.sep}`)) return path.join(home, input.slice(2));
284
+ return input;
285
+ }
286
+
287
+ /** @param {string} value @param {string} baseCwd */
288
+ export function resolveDirectoryValue(value, baseCwd) {
289
+ const raw = String(value || "").trim();
290
+ if (!raw) return existsDir(baseCwd) ? path.resolve(baseCwd) : null;
291
+ const expanded = expandHome(raw);
292
+ const resolved = path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(baseCwd, expanded);
293
+ return existsDir(resolved) ? resolved : null;
294
+ }
295
+
296
+ /**
297
+ * @param {string} query
298
+ * @param {string} baseCwd
299
+ * @param {number=} limit
300
+ * @returns {string[]}
301
+ */
302
+ export function listDirectorySuggestions(query, baseCwd, limit = 8) {
303
+ const raw = String(query || "").trim();
304
+ const expanded = expandHome(raw);
305
+ const resolved = raw ? (path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(baseCwd, expanded)) : path.resolve(baseCwd);
306
+ let searchDir = resolved;
307
+ let fragment = "";
308
+ if (!existsDir(resolved) || !raw.endsWith(path.sep)) {
309
+ searchDir = existsDir(resolved) ? resolved : path.dirname(resolved);
310
+ fragment = existsDir(resolved) ? "" : path.basename(resolved);
311
+ }
312
+ /** @type {string[]} */
313
+ const out = [];
314
+ if (existsDir(resolved)) out.push(path.resolve(resolved));
315
+ if (existsDir(searchDir)) {
316
+ const lowerFragment = fragment.toLowerCase();
317
+ const children = readdirSync(searchDir, { withFileTypes: true })
318
+ .filter((entry) => entry.isDirectory())
319
+ .map((entry) => entry.name)
320
+ .filter((name) => !lowerFragment || name.toLowerCase().includes(lowerFragment))
321
+ .sort((a, b) => a.localeCompare(b));
322
+ for (const name of children) out.push(path.join(searchDir, name));
323
+ }
324
+ return Array.from(new Set(out)).slice(0, Math.max(1, limit));
325
+ }
326
+
327
+ /** @param {string} dir */
328
+ function existsDir(dir) {
329
+ try {
330
+ return existsSync(dir) && statSync(dir).isDirectory();
331
+ } catch {
332
+ return false;
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Keep only candidates whose path is an existing directory.
338
+ * @param {CwdCandidate[]} candidates
339
+ * @returns {CwdCandidate[]}
340
+ */
341
+ export function existingCwdCandidates(candidates) {
342
+ return candidates.filter((entry) => existsDir(entry.path));
343
+ }
344
+
345
+ /**
346
+ * @typedef {Object} CwdCandidate
347
+ * @property {string} path
348
+ * @property {number} count
349
+ */
350
+
351
+ /**
352
+ * Filter ranked cwd candidates by case-insensitive substring match anywhere
353
+ * in the path (empty query keeps the full ranked list).
354
+ * @param {CwdCandidate[]} candidates
355
+ * @param {string} query
356
+ * @returns {CwdCandidate[]}
357
+ */
358
+ export function filterCwdCandidates(candidates, query) {
359
+ const q = String(query ?? "").trim().toLowerCase();
360
+ if (!q) return candidates;
361
+ return candidates.filter((entry) => entry.path.toLowerCase().includes(q));
362
+ }
363
+
364
+ /**
365
+ * Whether two path strings resolve to the same directory. Blank inputs never
366
+ * match (path.resolve("") would silently equal process.cwd()).
367
+ * @param {string} a
368
+ * @param {string} b
369
+ * @returns {boolean}
370
+ */
371
+ export function sameResolvedDir(a, b) {
372
+ const na = String(a ?? "").trim();
373
+ const nb = String(b ?? "").trim();
374
+ if (!na || !nb) return false;
375
+ return path.resolve(expandHome(na)) === path.resolve(expandHome(nb));
376
+ }
377
+
378
+ /**
379
+ * Browse-mode Tab completion for the cwd picker. Completes to the highlighted
380
+ * suggestion with a trailing separator (so the next recompute lists that
381
+ * directory's children); when the highlight already equals the current query
382
+ * (no progress), advances cyclically so repeated Tab drills downward. A lone
383
+ * self suggestion stays put, so drilling stops naturally at leaf directories.
384
+ * @param {string} query
385
+ * @param {string[]} suggestions
386
+ * @param {number} index
387
+ * @returns {{query: string, completed: string, usedIndex: number} | null}
388
+ */
389
+ export function browseTabCompletion(query, suggestions, index) {
390
+ if (!suggestions || suggestions.length === 0) return null;
391
+ const len = suggestions.length;
392
+ let usedIndex = ((index % len) + len) % len;
393
+ let completed = suggestions[usedIndex];
394
+ if (len > 1 && sameResolvedDir(completed, query)) {
395
+ usedIndex = (usedIndex + 1) % len;
396
+ completed = suggestions[usedIndex];
397
+ }
398
+ return { query: completed + path.sep, completed, usedIndex };
399
+ }
400
+
401
+ /**
402
+ * Decide cwd picker mode + suggestions for a query: favorites when the query
403
+ * is empty or matches ranked candidates, filesystem browse otherwise.
404
+ * @param {string} query
405
+ * @param {CwdCandidate[]} ranked
406
+ * @param {string} baseCwd
407
+ * @returns {{mode: "favorites"|"browse", suggestions: string[]}}
408
+ */
409
+ export function nextCwdPickerState(query, ranked, baseCwd) {
410
+ if (!ranked || ranked.length === 0) {
411
+ return { mode: "browse", suggestions: listDirectorySuggestions(query, baseCwd) };
412
+ }
413
+ const matches = filterCwdCandidates(ranked, query);
414
+ if (String(query ?? "").trim() === "" || matches.length > 0) {
415
+ return { mode: "favorites", suggestions: matches.map((entry) => entry.path) };
416
+ }
417
+ return { mode: "browse", suggestions: listDirectorySuggestions(query, baseCwd) };
418
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Launch a detached job-runner process for one run.
3
+ *
4
+ * The runner is a plain `.mjs` spawned with `node`, fully detached (its own process
5
+ * group, stdio ignored) so it survives the parent Pi reloading or exiting. The parent
6
+ * records the runner pid in `pid.json` and watches the store files the runner writes.
7
+ */
8
+ import { spawn } from "node:child_process";
9
+ import { dirname, resolve } from "node:path";
10
+ import { atomicWriteJson } from "./atomic.mjs";
11
+ import { resolveNode } from "./invocation.mjs";
12
+ import * as P from "./paths.mjs";
13
+ import { writePid } from "./store.mjs";
14
+
15
+ /** @typedef {import("./types.mjs").RunConfig} RunConfig */
16
+ /** @typedef {import("./types.mjs").HostConfig} HostConfig */
17
+ /** @typedef {import("./types.mjs").TitleConfig} TitleConfig */
18
+ /** @typedef {import("./types.mjs").AutoStateConfig} AutoStateConfig */
19
+
20
+ /**
21
+ * Spawn a fully detached runner child with async spawn failures made harmless.
22
+ *
23
+ * A spawn that fails to start (e.g. a transient ENOENT on the node binary,
24
+ * issue #86) reports asynchronously via the 'error' event; without a listener
25
+ * the EventEmitter rethrows it as an uncaughtException and takes down the
26
+ * whole host Pi process. Callers already handle the failure gracefully via
27
+ * the pid == null branch (state "failed").
28
+ * @param {string} command
29
+ * @param {string[]} args
30
+ * @param {string} cwd
31
+ * @returns {import("node:child_process").ChildProcess}
32
+ */
33
+ function spawnDetached(command, args, cwd) {
34
+ const child = spawn(command, args, {
35
+ cwd,
36
+ detached: true,
37
+ stdio: "ignore",
38
+ env: process.env,
39
+ // Windows: detached children get their own console window unless
40
+ // suppressed (CREATE_NO_WINDOW; no-op on POSIX) — issue #49.
41
+ windowsHide: true,
42
+ });
43
+ child.on("error", () => {});
44
+ child.unref();
45
+ return child;
46
+ }
47
+
48
+ /**
49
+ * @param {string} root
50
+ * @param {RunConfig} config
51
+ * @param {{ runnerScript: string, node?: string }} opts
52
+ * @returns {{ pid: number|null, configPath: string }}
53
+ */
54
+ export function launchRun(root, config, opts) {
55
+ const runDir = P.runDir(root, config.viewId, config.runId);
56
+ const configPath = `${runDir}/config.json`;
57
+ atomicWriteJson(configPath, config);
58
+
59
+ const node = opts.node ?? resolveNode();
60
+ const child = spawnDetached(node, [opts.runnerScript, configPath], config.cwd);
61
+
62
+ const pid = child.pid ?? null;
63
+ // Record the *runner/monitor* pid for liveness polling (the worker pid is tracked
64
+ // inside status.json by the runner itself).
65
+ writePid(root, config.viewId, config.runId, pid);
66
+ return { pid, configPath };
67
+ }
68
+
69
+ /**
70
+ * Launch a detached PTY host for a view. The host owns a long-lived child Pi and
71
+ * exposes a JSONL control socket for attach/input/resize/terminate.
72
+ * @param {string} root
73
+ * @param {HostConfig} config
74
+ * @param {{ runnerScript: string, node?: string }} opts
75
+ * @returns {{ pid: number|null, configPath: string }}
76
+ */
77
+ export function launchHost(root, config, opts) {
78
+ // Instance-specific config path wins when present (issue #70: concurrent
79
+ // launches must not share the fixed host-config.json); legacy callers keep it.
80
+ const configPath = config.configPath ?? P.hostConfigPath(root, config.viewId);
81
+ atomicWriteJson(configPath, config);
82
+
83
+ const node = opts.node ?? resolveNode();
84
+ const child = spawnDetached(node, [opts.runnerScript, configPath], config.cwd);
85
+
86
+ return { pid: child.pid ?? null, configPath };
87
+ }
88
+
89
+ /**
90
+ * Launch a detached title runner for a view. Best-effort only: it may update `meta.json`
91
+ * later with a short GPT-generated name derived from the initial task prompt.
92
+ * @param {string} root
93
+ * @param {TitleConfig} config
94
+ * @param {{ runnerScript: string, node?: string }} opts
95
+ * @returns {{ pid: number|null, configPath: string }}
96
+ */
97
+ export function launchTitle(root, config, opts) {
98
+ const configPath = P.titleConfigPath(root, config.viewId);
99
+ atomicWriteJson(configPath, config);
100
+
101
+ const node = opts.node ?? resolveNode();
102
+ const child = spawnDetached(node, [opts.runnerScript, configPath], config.cwd);
103
+
104
+ return { pid: child.pid ?? null, configPath };
105
+ }
106
+
107
+ /**
108
+ * Launch a detached auto-state classifier for a view. Best-effort: it may update
109
+ * state.json later with a model-refined terminal bucket.
110
+ * @param {string} root
111
+ * @param {AutoStateConfig} config
112
+ * @param {{ runnerScript: string, node?: string }} opts
113
+ * @returns {{ pid: number|null, configPath: string }}
114
+ */
115
+ export function launchAutoState(root, config, opts) {
116
+ const configPath = P.autoStateConfigPath(root, config.viewId);
117
+ atomicWriteJson(configPath, config);
118
+
119
+ const node = opts.node ?? resolveNode();
120
+ const child = spawnDetached(node, [opts.runnerScript, configPath], config.cwd);
121
+
122
+ return { pid: child.pid ?? null, configPath };
123
+ }
124
+
125
+ /**
126
+ * Compute the detached coordinator spawn plan (issue #241): an absolute root
127
+ * argv and a cwd OUTSIDE the root (the root's parent; at a drive/filesystem
128
+ * root the parent is the root itself). The coordinator derives all paths from
129
+ * the root argument and never uses cwd, so keeping cwd off the root releases
130
+ * the win32 directory handle that made throwaway roots undeletable.
131
+ * @param {string} root
132
+ * @returns {{ root: string, cwd: string }}
133
+ */
134
+ export function coordinatorSpawnPlan(root) {
135
+ const absoluteRoot = resolve(root);
136
+ return { root: absoluteRoot, cwd: dirname(absoluteRoot) };
137
+ }
138
+
139
+ /**
140
+ * Launch the detached view-state coordinator for a board root (issue #91, spec
141
+ * D3). No config file: the coordinator takes the root as its only argument.
142
+ * Idempotent by lease — a second instance loses the coordinator lease and
143
+ * exits silently, so callers may spawn freely on probe failure.
144
+ * The child's cwd is the root's PARENT (issue #241): the coordinator derives
145
+ * paths from argv and cwd must not pin the root on win32. At a drive/filesystem
146
+ * root (win32 `C:\`, POSIX `/`) the parent is the root itself — see
147
+ * `coordinatorSpawnPlan`.
148
+ * @param {string} root
149
+ * @param {{ runnerScript: string, node?: string }} opts
150
+ * @returns {{ pid: number|null }}
151
+ */
152
+ export function launchCoordinator(root, opts) {
153
+ const node = opts.node ?? resolveNode();
154
+ const plan = coordinatorSpawnPlan(root);
155
+ const child = spawnDetached(node, [opts.runnerScript, plan.root], plan.cwd);
156
+ return { pid: child.pid ?? null };
157
+ }
@@ -0,0 +1,4 @@
1
+ /** Imported first, because some modules read their settings as they load. */
2
+ import { applyConfig } from "./config.mjs";
3
+
4
+ applyConfig();