@zerwiz/ymir 0.1.34 → 0.1.35

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 (929) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/apps/README.md +4 -0
  3. package/apps/hlidskjalf/README.md +68 -0
  4. package/apps/hlidskjalf/android/app/build.gradle +73 -0
  5. package/apps/hlidskjalf/android/app/capacitor.build.gradle +19 -0
  6. package/apps/hlidskjalf/android/app/proguard-rules.pro +21 -0
  7. package/apps/hlidskjalf/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java +26 -0
  8. package/apps/hlidskjalf/android/app/src/main/AndroidManifest.xml +41 -0
  9. package/apps/hlidskjalf/android/app/src/main/java/org/zerwiz/ymir/MainActivity.java +5 -0
  10. package/apps/hlidskjalf/android/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
  11. package/apps/hlidskjalf/android/app/src/main/res/drawable/splash.png +0 -0
  12. package/apps/hlidskjalf/android/app/src/main/res/drawable-land-hdpi/splash.png +0 -0
  13. package/apps/hlidskjalf/android/app/src/main/res/drawable-land-mdpi/splash.png +0 -0
  14. package/apps/hlidskjalf/android/app/src/main/res/drawable-land-xhdpi/splash.png +0 -0
  15. package/apps/hlidskjalf/android/app/src/main/res/drawable-land-xxhdpi/splash.png +0 -0
  16. package/apps/hlidskjalf/android/app/src/main/res/drawable-land-xxxhdpi/splash.png +0 -0
  17. package/apps/hlidskjalf/android/app/src/main/res/drawable-port-hdpi/splash.png +0 -0
  18. package/apps/hlidskjalf/android/app/src/main/res/drawable-port-mdpi/splash.png +0 -0
  19. package/apps/hlidskjalf/android/app/src/main/res/drawable-port-xhdpi/splash.png +0 -0
  20. package/apps/hlidskjalf/android/app/src/main/res/drawable-port-xxhdpi/splash.png +0 -0
  21. package/apps/hlidskjalf/android/app/src/main/res/drawable-port-xxxhdpi/splash.png +0 -0
  22. package/apps/hlidskjalf/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +34 -0
  23. package/apps/hlidskjalf/android/app/src/main/res/layout/activity_main.xml +12 -0
  24. package/apps/hlidskjalf/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  25. package/apps/hlidskjalf/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
  26. package/apps/hlidskjalf/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  27. package/apps/hlidskjalf/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  28. package/apps/hlidskjalf/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  29. package/apps/hlidskjalf/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  30. package/apps/hlidskjalf/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  31. package/apps/hlidskjalf/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  32. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  33. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  34. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  35. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  36. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  37. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  38. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  39. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  40. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  41. package/apps/hlidskjalf/android/app/src/main/res/values/ic_launcher_background.xml +4 -0
  42. package/apps/hlidskjalf/android/app/src/main/res/values/strings.xml +7 -0
  43. package/apps/hlidskjalf/android/app/src/main/res/values/styles.xml +22 -0
  44. package/apps/hlidskjalf/android/app/src/main/res/xml/file_paths.xml +5 -0
  45. package/apps/hlidskjalf/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java +18 -0
  46. package/apps/hlidskjalf/android/build.gradle +29 -0
  47. package/apps/hlidskjalf/android/capacitor.settings.gradle +3 -0
  48. package/apps/hlidskjalf/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  49. package/apps/hlidskjalf/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  50. package/apps/hlidskjalf/android/gradle.properties +22 -0
  51. package/apps/hlidskjalf/android/gradlew +248 -0
  52. package/apps/hlidskjalf/android/gradlew.bat +92 -0
  53. package/apps/hlidskjalf/android/settings.gradle +5 -0
  54. package/apps/hlidskjalf/android/variables.gradle +16 -0
  55. package/apps/hlidskjalf/capacitor.config.ts +26 -0
  56. package/apps/hlidskjalf/dist/apple-touch-icon.png +0 -0
  57. package/apps/hlidskjalf/dist/assets/index-BWmNIyaG.css +1 -0
  58. package/apps/hlidskjalf/dist/assets/index-k7QEfNto.js +88 -0
  59. package/apps/hlidskjalf/dist/assets/index-k7QEfNto.js.map +1 -0
  60. package/apps/hlidskjalf/dist/icon.svg +7 -0
  61. package/apps/hlidskjalf/dist/index.html +64 -0
  62. package/apps/hlidskjalf/dist/manifest.webmanifest +31 -0
  63. package/apps/hlidskjalf/dist/og.jpg +0 -0
  64. package/apps/hlidskjalf/dist/og.png +0 -0
  65. package/apps/hlidskjalf/dist/og.svg +64 -0
  66. package/apps/hlidskjalf/dist/sw.js +39 -0
  67. package/apps/hlidskjalf/dist/ymir-icon.png +0 -0
  68. package/apps/hlidskjalf/dist/ymir-mark.svg +62 -0
  69. package/apps/hlidskjalf/electron/icon.png +0 -0
  70. package/apps/hlidskjalf/electron/main.cjs +236 -0
  71. package/apps/hlidskjalf/electron/preload.cjs +8 -0
  72. package/apps/hlidskjalf/electron/smidja-icon.png +0 -0
  73. package/apps/hlidskjalf/electron/ymir-hlidskjalf.desktop.in +12 -0
  74. package/apps/hlidskjalf/electron/ymir-smidja.desktop.in +12 -0
  75. package/apps/hlidskjalf/index.html +63 -0
  76. package/apps/hlidskjalf/package-lock.json +5398 -0
  77. package/apps/hlidskjalf/package.json +55 -0
  78. package/apps/hlidskjalf/public/apple-touch-icon.png +0 -0
  79. package/apps/hlidskjalf/public/icon.svg +7 -0
  80. package/apps/hlidskjalf/public/manifest.webmanifest +31 -0
  81. package/apps/hlidskjalf/public/og.jpg +0 -0
  82. package/apps/hlidskjalf/public/og.png +0 -0
  83. package/apps/hlidskjalf/public/og.svg +64 -0
  84. package/apps/hlidskjalf/public/sw.js +39 -0
  85. package/apps/hlidskjalf/public/ymir-icon.png +0 -0
  86. package/apps/hlidskjalf/public/ymir-mark.svg +62 -0
  87. package/apps/hlidskjalf/server/accounts.ts +159 -0
  88. package/apps/hlidskjalf/server/index.ts +2045 -0
  89. package/apps/hlidskjalf/src/app/App.tsx +116 -0
  90. package/apps/hlidskjalf/src/app/BottomStream.tsx +128 -0
  91. package/apps/hlidskjalf/src/app/MobileNav.tsx +87 -0
  92. package/apps/hlidskjalf/src/app/Rail.tsx +106 -0
  93. package/apps/hlidskjalf/src/app/Shell.tsx +135 -0
  94. package/apps/hlidskjalf/src/app/Topbar.tsx +130 -0
  95. package/apps/hlidskjalf/src/components/AccentPicker.tsx +207 -0
  96. package/apps/hlidskjalf/src/components/AccountMenu.tsx +115 -0
  97. package/apps/hlidskjalf/src/components/AgentCard.tsx +75 -0
  98. package/apps/hlidskjalf/src/components/EmberBackground.tsx +36 -0
  99. package/apps/hlidskjalf/src/components/Emblem.tsx +48 -0
  100. package/apps/hlidskjalf/src/components/Halls.tsx +155 -0
  101. package/apps/hlidskjalf/src/components/LoginModal.tsx +329 -0
  102. package/apps/hlidskjalf/src/components/Markdown.tsx +10 -0
  103. package/apps/hlidskjalf/src/components/MetricTile.tsx +40 -0
  104. package/apps/hlidskjalf/src/components/ModelPicker.tsx +100 -0
  105. package/apps/hlidskjalf/src/components/Overlay.tsx +134 -0
  106. package/apps/hlidskjalf/src/components/PRCard.tsx +154 -0
  107. package/apps/hlidskjalf/src/components/RecallPanel.tsx +116 -0
  108. package/apps/hlidskjalf/src/components/RuneTag.tsx +23 -0
  109. package/apps/hlidskjalf/src/components/Status.tsx +35 -0
  110. package/apps/hlidskjalf/src/components/TaskChip.tsx +23 -0
  111. package/apps/hlidskjalf/src/components/TraceRow.tsx +28 -0
  112. package/apps/hlidskjalf/src/data/lore.ts +127 -0
  113. package/apps/hlidskjalf/src/data/metadata.ts +147 -0
  114. package/apps/hlidskjalf/src/data/mythology.ts +95 -0
  115. package/apps/hlidskjalf/src/data/realms.ts +167 -0
  116. package/apps/hlidskjalf/src/gates/Cron.tsx +77 -0
  117. package/apps/hlidskjalf/src/gates/Decisions.tsx +67 -0
  118. package/apps/hlidskjalf/src/gates/Files.tsx +138 -0
  119. package/apps/hlidskjalf/src/gates/Fleet.tsx +184 -0
  120. package/apps/hlidskjalf/src/gates/Forge.tsx +486 -0
  121. package/apps/hlidskjalf/src/gates/OmniChat.tsx +217 -0
  122. package/apps/hlidskjalf/src/gates/Processes.tsx +162 -0
  123. package/apps/hlidskjalf/src/gates/Profile.tsx +225 -0
  124. package/apps/hlidskjalf/src/gates/Reviews.tsx +33 -0
  125. package/apps/hlidskjalf/src/gates/Runes.tsx +116 -0
  126. package/apps/hlidskjalf/src/gates/Runtime.tsx +106 -0
  127. package/apps/hlidskjalf/src/gates/Sessions.tsx +109 -0
  128. package/apps/hlidskjalf/src/gates/Stats.tsx +268 -0
  129. package/apps/hlidskjalf/src/gates/Tasks.tsx +173 -0
  130. package/apps/hlidskjalf/src/gates/Trace.tsx +155 -0
  131. package/apps/hlidskjalf/src/gates/Well.tsx +154 -0
  132. package/apps/hlidskjalf/src/gates/Worktrees.tsx +48 -0
  133. package/apps/hlidskjalf/src/hooks/useTenantDef.ts +13 -0
  134. package/apps/hlidskjalf/src/main.tsx +31 -0
  135. package/apps/hlidskjalf/src/services/api.ts +404 -0
  136. package/apps/hlidskjalf/src/services/auth.ts +107 -0
  137. package/apps/hlidskjalf/src/services/stream.ts +35 -0
  138. package/apps/hlidskjalf/src/state/store.ts +648 -0
  139. package/apps/hlidskjalf/src/state/ui.ts +62 -0
  140. package/apps/hlidskjalf/src/styles/auth.css +485 -0
  141. package/apps/hlidskjalf/src/styles/components.css +1259 -0
  142. package/apps/hlidskjalf/src/styles/forge.css +115 -0
  143. package/apps/hlidskjalf/src/styles/global.css +167 -0
  144. package/apps/hlidskjalf/src/styles/menus.css +175 -0
  145. package/apps/hlidskjalf/src/styles/mobile.css +298 -0
  146. package/apps/hlidskjalf/src/styles/overlays.css +205 -0
  147. package/apps/hlidskjalf/src/styles/shell.css +641 -0
  148. package/apps/hlidskjalf/src/types.ts +251 -0
  149. package/apps/hlidskjalf/src/vite-env.d.ts +9 -0
  150. package/apps/hlidskjalf/static/landing.md +6 -0
  151. package/apps/hlidskjalf/tsconfig.json +25 -0
  152. package/apps/hlidskjalf/vite.config.ts +49 -0
  153. package/apps/hlidskjalf-mobile/App.tsx +93 -0
  154. package/apps/hlidskjalf-mobile/app.json +13 -0
  155. package/apps/hlidskjalf-mobile/index.ts +4 -0
  156. package/apps/hlidskjalf-mobile/package.json +35 -0
  157. package/apps/hlidskjalf-mobile/public/icon.svg +7 -0
  158. package/apps/hlidskjalf-mobile/tsconfig.json +4 -0
  159. package/apps/odrerir/.astro/content.d.ts +159 -0
  160. package/apps/odrerir/.astro/dev.json +24 -0
  161. package/apps/odrerir/.astro/settings.json +5 -0
  162. package/apps/odrerir/.astro/types.d.ts +2 -0
  163. package/apps/odrerir/astro.config.mjs +79 -0
  164. package/apps/odrerir/dist/apple-touch-icon.png +0 -0
  165. package/apps/odrerir/dist/favicon-192.png +0 -0
  166. package/apps/odrerir/dist/favicon-32.png +0 -0
  167. package/apps/odrerir/dist/favicon-512.png +0 -0
  168. package/apps/odrerir/dist/favicon.svg +53 -0
  169. package/apps/odrerir/dist/icon.svg +7 -0
  170. package/apps/odrerir/dist/index.html +837 -0
  171. package/apps/odrerir/dist/og-square.png +0 -0
  172. package/apps/odrerir/dist/og-story.png +0 -0
  173. package/apps/odrerir/dist/og.png +0 -0
  174. package/apps/odrerir/dist/safari-pinned-tab.svg +7 -0
  175. package/apps/odrerir/dist/site.webmanifest +16 -0
  176. package/apps/odrerir/dist/styles/cloth.css +112 -0
  177. package/apps/odrerir/dist/styles/livehall.css +278 -0
  178. package/apps/odrerir/electron/icon.png +0 -0
  179. package/apps/odrerir/electron/main.cjs +183 -0
  180. package/apps/odrerir/electron/ymir-odrerir.desktop.in +12 -0
  181. package/apps/odrerir/package-lock.json +4195 -0
  182. package/apps/odrerir/package.json +30 -0
  183. package/apps/odrerir/public/apple-touch-icon.png +0 -0
  184. package/apps/odrerir/public/favicon-192.png +0 -0
  185. package/apps/odrerir/public/favicon-32.png +0 -0
  186. package/apps/odrerir/public/favicon-512.png +0 -0
  187. package/apps/odrerir/public/favicon.svg +53 -0
  188. package/apps/odrerir/public/icon.svg +7 -0
  189. package/apps/odrerir/public/og-square.png +0 -0
  190. package/apps/odrerir/public/og-story.png +0 -0
  191. package/apps/odrerir/public/og.png +0 -0
  192. package/apps/odrerir/public/safari-pinned-tab.svg +7 -0
  193. package/apps/odrerir/public/site.webmanifest +16 -0
  194. package/apps/odrerir/scripts/extract-cloth.mjs +101 -0
  195. package/apps/odrerir/src/index.html +837 -0
  196. package/apps/odrerir/src/styles/cloth.css +112 -0
  197. package/apps/odrerir/src/styles/livehall.css +278 -0
  198. package/apps/sessrumnir/.github/ISSUE_TEMPLATE/bug_report.yml +99 -0
  199. package/apps/sessrumnir/.github/ISSUE_TEMPLATE/config.yml +8 -0
  200. package/apps/sessrumnir/.github/ISSUE_TEMPLATE/feature_request.yml +46 -0
  201. package/apps/sessrumnir/.github/PULL_REQUEST_TEMPLATE.md +56 -0
  202. package/apps/sessrumnir/.github/SUPPORT.md +18 -0
  203. package/apps/sessrumnir/.github/workflows/build.yml +133 -0
  204. package/apps/sessrumnir/AGENTS.md +540 -0
  205. package/apps/sessrumnir/CLA.md +63 -0
  206. package/apps/sessrumnir/CONTRIBUTING.md +233 -0
  207. package/apps/sessrumnir/LICENSE +200 -0
  208. package/apps/sessrumnir/NOTICE +33 -0
  209. package/apps/sessrumnir/README.md +329 -0
  210. package/apps/sessrumnir/bin/sessrumnir-web.sh +117 -0
  211. package/apps/sessrumnir/bin/sessrumnir.js +169 -0
  212. package/apps/sessrumnir/docs/CNAME +1 -0
  213. package/apps/sessrumnir/docs/favicon.ico +0 -0
  214. package/apps/sessrumnir/docs/icon-512.png +0 -0
  215. package/apps/sessrumnir/docs/index.html +441 -0
  216. package/apps/sessrumnir/docs/robots.txt +4 -0
  217. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_181929.png +0 -0
  218. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182005.png +0 -0
  219. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182020.png +0 -0
  220. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182039.png +0 -0
  221. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182117.png +0 -0
  222. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182225.png +0 -0
  223. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182248.png +0 -0
  224. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182322.png +0 -0
  225. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182340.png +0 -0
  226. package/apps/sessrumnir/docs/sitemap.xml +9 -0
  227. package/apps/sessrumnir/electron.vite.config.ts +49 -0
  228. package/apps/sessrumnir/eslint.config.mjs +75 -0
  229. package/apps/sessrumnir/i18next.config.ts +33 -0
  230. package/apps/sessrumnir/install.sh +144 -0
  231. package/apps/sessrumnir/out/main/index.js +10881 -0
  232. package/apps/sessrumnir/out/preload/index.js +722 -0
  233. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-400-normal-C8QS47vb.woff2 +0 -0
  234. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-400-normal-D3EsxgFc.woff +0 -0
  235. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-500-normal-B7dJQtg-.woff +0 -0
  236. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-500-normal-BLlg2W5x.woff2 +0 -0
  237. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-600-normal-BcIsDXzC.woff2 +0 -0
  238. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-600-normal-CBymlcNF.woff +0 -0
  239. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-400-normal-Bgrpe4p1.woff +0 -0
  240. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-400-normal-BlcaxZtM.woff2 +0 -0
  241. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-500-normal-CdQuyvtc.woff +0 -0
  242. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-500-normal-pZw22qtS.woff2 +0 -0
  243. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-600-normal-Dr-wLyOm.woff +0 -0
  244. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-600-normal-dLR2-a_v.woff2 +0 -0
  245. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-400-normal-BGH8Vunh.woff2 +0 -0
  246. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-400-normal-C3_-2Ua-.woff +0 -0
  247. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-500-normal-Dj3SQ6fR.woff +0 -0
  248. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-500-normal-EBdSCOD3.woff2 +0 -0
  249. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-600-normal-B254Xisw.woff2 +0 -0
  250. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-600-normal-DUW-TI76.woff +0 -0
  251. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-400-normal-DuQ88yz3.woff2 +0 -0
  252. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-400-normal-DuXFa1Dr.woff +0 -0
  253. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-500-normal-AH9qog1s.woff2 +0 -0
  254. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-500-normal-DAuUCO41.woff +0 -0
  255. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-600-normal-CD-dlDpD.woff +0 -0
  256. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-600-normal-D3T6BBXG.woff2 +0 -0
  257. package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-400-normal-C-RiYxEf.woff2 +0 -0
  258. package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-400-normal-DmUuA7Y2.woff +0 -0
  259. package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-500-normal-DsPuwQHi.woff2 +0 -0
  260. package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-500-normal-tGBW_mI7.woff +0 -0
  261. package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-600-normal-Dar_cCT6.woff2 +0 -0
  262. package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-600-normal-GIs8CF2x.woff +0 -0
  263. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-300-normal-Ba-HN6uq.woff +0 -0
  264. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-300-normal-ByB2hQUF.woff2 +0 -0
  265. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  266. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  267. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  268. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  269. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-300-normal-B8O3uHbr.woff +0 -0
  270. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-300-normal-BbtBtHoB.woff2 +0 -0
  271. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  272. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  273. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  274. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  275. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-300-normal-BPU54Tw3.woff +0 -0
  276. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-300-normal-BRS-C4KA.woff2 +0 -0
  277. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  278. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  279. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  280. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  281. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-300-normal-BEtQ2wm2.woff2 +0 -0
  282. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-300-normal-CQbCaAQ4.woff +0 -0
  283. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  284. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  285. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  286. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  287. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-300-normal-B6fSpYYV.woff +0 -0
  288. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-300-normal-CpdbWO_2.woff2 +0 -0
  289. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  290. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  291. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  292. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  293. package/apps/sessrumnir/out/renderer/assets/index-BxqsRq2x.css +4858 -0
  294. package/apps/sessrumnir/out/renderer/assets/index-TaD69rFk.js +99435 -0
  295. package/apps/sessrumnir/out/renderer/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  296. package/apps/sessrumnir/out/renderer/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  297. package/apps/sessrumnir/out/renderer/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  298. package/apps/sessrumnir/out/renderer/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  299. package/apps/sessrumnir/out/renderer/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  300. package/apps/sessrumnir/out/renderer/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  301. package/apps/sessrumnir/out/renderer/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  302. package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  303. package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  304. package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  305. package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  306. package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  307. package/apps/sessrumnir/out/renderer/assets/newsreader-latin-ext-wght-italic-DFS986qL.woff2 +0 -0
  308. package/apps/sessrumnir/out/renderer/assets/newsreader-latin-ext-wght-normal-C-3rgBeH.woff2 +0 -0
  309. package/apps/sessrumnir/out/renderer/assets/newsreader-latin-wght-italic-Bxi8ein9.woff2 +0 -0
  310. package/apps/sessrumnir/out/renderer/assets/newsreader-latin-wght-normal-CCVVNp6i.woff2 +0 -0
  311. package/apps/sessrumnir/out/renderer/assets/newsreader-vietnamese-wght-italic-xAqemagA.woff2 +0 -0
  312. package/apps/sessrumnir/out/renderer/assets/newsreader-vietnamese-wght-normal-Czsa-EzN.woff2 +0 -0
  313. package/apps/sessrumnir/out/renderer/assets/openmoji-color-9qo_2DLG.woff2 +0 -0
  314. package/apps/sessrumnir/out/renderer/assets/pi-logo-B6SVnhuS.svg +7 -0
  315. package/apps/sessrumnir/out/renderer/index.html +19 -0
  316. package/apps/sessrumnir/out/renderer/theme-boot.js +30 -0
  317. package/apps/sessrumnir/package-lock.json +10636 -0
  318. package/apps/sessrumnir/package.json +183 -0
  319. package/apps/sessrumnir/public/apple-touch-icon.png +0 -0
  320. package/apps/sessrumnir/public/icon.svg +7 -0
  321. package/apps/sessrumnir/resources/icons/generate_icons.py +38 -0
  322. package/apps/sessrumnir/resources/icons/icon-128.png +0 -0
  323. package/apps/sessrumnir/resources/icons/icon-16.png +0 -0
  324. package/apps/sessrumnir/resources/icons/icon-256.png +0 -0
  325. package/apps/sessrumnir/resources/icons/icon-32.png +0 -0
  326. package/apps/sessrumnir/resources/icons/icon-48.png +0 -0
  327. package/apps/sessrumnir/resources/icons/icon-512.png +0 -0
  328. package/apps/sessrumnir/resources/icons/icon-64.png +0 -0
  329. package/apps/sessrumnir/resources/icons/icon.icns +0 -0
  330. package/apps/sessrumnir/resources/icons/icon.ico +0 -0
  331. package/apps/sessrumnir/resources/icons/icon.png +0 -0
  332. package/apps/sessrumnir/resources/icons/icon.svg +7 -0
  333. package/apps/sessrumnir/resources/linux/after-install.sh +23 -0
  334. package/apps/sessrumnir/resources/linux/after-remove.sh +18 -0
  335. package/apps/sessrumnir/resources/locales/en/translation.json +1735 -0
  336. package/apps/sessrumnir/resources/permission-prompt-text.test.ts +111 -0
  337. package/apps/sessrumnir/resources/permission-prompt-text.ts +84 -0
  338. package/apps/sessrumnir/resources/permission-rules.test.ts +351 -0
  339. package/apps/sessrumnir/resources/permission-rules.ts +293 -0
  340. package/apps/sessrumnir/resources/pi-desktop-permissions.ts +62 -0
  341. package/apps/sessrumnir/resources/ymir-sessrumnir.desktop.in +12 -0
  342. package/apps/sessrumnir/scripts/check-semantic-colors.mjs +57 -0
  343. package/apps/sessrumnir/src/main/activity-stats.test.ts +240 -0
  344. package/apps/sessrumnir/src/main/activity-stats.ts +596 -0
  345. package/apps/sessrumnir/src/main/agent-detection.test.ts +25 -0
  346. package/apps/sessrumnir/src/main/agent-detection.ts +114 -0
  347. package/apps/sessrumnir/src/main/app-data-paths.test.ts +90 -0
  348. package/apps/sessrumnir/src/main/app-data-paths.ts +95 -0
  349. package/apps/sessrumnir/src/main/app-log.test.ts +114 -0
  350. package/apps/sessrumnir/src/main/app-log.ts +210 -0
  351. package/apps/sessrumnir/src/main/archived-sessions.test.ts +50 -0
  352. package/apps/sessrumnir/src/main/archived-sessions.ts +95 -0
  353. package/apps/sessrumnir/src/main/attachment-reader.test.ts +56 -0
  354. package/apps/sessrumnir/src/main/attachment-reader.ts +52 -0
  355. package/apps/sessrumnir/src/main/auto-tag.test.ts +38 -0
  356. package/apps/sessrumnir/src/main/auto-tag.ts +75 -0
  357. package/apps/sessrumnir/src/main/autostart-linux.test.ts +56 -0
  358. package/apps/sessrumnir/src/main/autostart-linux.ts +58 -0
  359. package/apps/sessrumnir/src/main/cmd-escape.test.ts +424 -0
  360. package/apps/sessrumnir/src/main/cmd-escape.ts +186 -0
  361. package/apps/sessrumnir/src/main/council-manager.test.ts +167 -0
  362. package/apps/sessrumnir/src/main/council-manager.ts +265 -0
  363. package/apps/sessrumnir/src/main/diagnostics-report.test.ts +140 -0
  364. package/apps/sessrumnir/src/main/diagnostics-report.ts +91 -0
  365. package/apps/sessrumnir/src/main/diagnostics.ts +111 -0
  366. package/apps/sessrumnir/src/main/editor-guard.test.ts +59 -0
  367. package/apps/sessrumnir/src/main/editor-guard.ts +50 -0
  368. package/apps/sessrumnir/src/main/extension-ui-ipc.test.ts +299 -0
  369. package/apps/sessrumnir/src/main/extension-ui-ipc.ts +117 -0
  370. package/apps/sessrumnir/src/main/file-service.test.ts +279 -0
  371. package/apps/sessrumnir/src/main/file-service.ts +652 -0
  372. package/apps/sessrumnir/src/main/fs-errors.ts +42 -0
  373. package/apps/sessrumnir/src/main/get-messages-trim.test.ts +77 -0
  374. package/apps/sessrumnir/src/main/get-messages-trim.ts +97 -0
  375. package/apps/sessrumnir/src/main/git-conveyor.test.ts +305 -0
  376. package/apps/sessrumnir/src/main/git-conveyor.ts +338 -0
  377. package/apps/sessrumnir/src/main/git-worktree.test.ts +101 -0
  378. package/apps/sessrumnir/src/main/git-worktree.ts +201 -0
  379. package/apps/sessrumnir/src/main/i18n.ts +31 -0
  380. package/apps/sessrumnir/src/main/index.ts +619 -0
  381. package/apps/sessrumnir/src/main/ipc/active-engine.ts +15 -0
  382. package/apps/sessrumnir/src/main/ipc/context.ts +58 -0
  383. package/apps/sessrumnir/src/main/ipc/council-handlers.ts +126 -0
  384. package/apps/sessrumnir/src/main/ipc/diagnostics-handlers.ts +8 -0
  385. package/apps/sessrumnir/src/main/ipc/file-handlers.ts +96 -0
  386. package/apps/sessrumnir/src/main/ipc/git-conveyor-handlers.ts +55 -0
  387. package/apps/sessrumnir/src/main/ipc/model-handlers.ts +33 -0
  388. package/apps/sessrumnir/src/main/ipc/models-config-handlers.ts +80 -0
  389. package/apps/sessrumnir/src/main/ipc/notes-handlers.ts +65 -0
  390. package/apps/sessrumnir/src/main/ipc/package-handlers.ts +330 -0
  391. package/apps/sessrumnir/src/main/ipc/permission-rules-handlers.ts +223 -0
  392. package/apps/sessrumnir/src/main/ipc/pi-handlers.ts +136 -0
  393. package/apps/sessrumnir/src/main/ipc/pi-start-options.ts +150 -0
  394. package/apps/sessrumnir/src/main/ipc/run-pi-cli.ts +53 -0
  395. package/apps/sessrumnir/src/main/ipc/session-handlers.ts +491 -0
  396. package/apps/sessrumnir/src/main/ipc/settings.ts +120 -0
  397. package/apps/sessrumnir/src/main/ipc/skills-mcp-handlers.ts +192 -0
  398. package/apps/sessrumnir/src/main/ipc/system-handlers.ts +131 -0
  399. package/apps/sessrumnir/src/main/ipc/tag-handlers.ts +69 -0
  400. package/apps/sessrumnir/src/main/ipc/terminal-handlers.ts +41 -0
  401. package/apps/sessrumnir/src/main/ipc/theme-handlers.ts +131 -0
  402. package/apps/sessrumnir/src/main/ipc/update-handlers.ts +61 -0
  403. package/apps/sessrumnir/src/main/ipc/validation.ts +45 -0
  404. package/apps/sessrumnir/src/main/ipc/workflow-handlers.ts +108 -0
  405. package/apps/sessrumnir/src/main/ipc/workspace-activity-wiring.ts +179 -0
  406. package/apps/sessrumnir/src/main/ipc/workspace-handlers.ts +133 -0
  407. package/apps/sessrumnir/src/main/ipc-handlers.ts +98 -0
  408. package/apps/sessrumnir/src/main/map-concurrent.test.ts +80 -0
  409. package/apps/sessrumnir/src/main/map-concurrent.ts +30 -0
  410. package/apps/sessrumnir/src/main/models-file.test.ts +114 -0
  411. package/apps/sessrumnir/src/main/models-file.ts +89 -0
  412. package/apps/sessrumnir/src/main/notes-manager.test.ts +64 -0
  413. package/apps/sessrumnir/src/main/notes-manager.ts +183 -0
  414. package/apps/sessrumnir/src/main/notify-decision.test.ts +39 -0
  415. package/apps/sessrumnir/src/main/notify-decision.ts +25 -0
  416. package/apps/sessrumnir/src/main/omp-fork-points.test.ts +119 -0
  417. package/apps/sessrumnir/src/main/omp-fork-points.ts +105 -0
  418. package/apps/sessrumnir/src/main/omp-plugin-list.test.ts +94 -0
  419. package/apps/sessrumnir/src/main/omp-plugin-list.ts +103 -0
  420. package/apps/sessrumnir/src/main/package-catalog.test.ts +157 -0
  421. package/apps/sessrumnir/src/main/package-catalog.ts +166 -0
  422. package/apps/sessrumnir/src/main/package-updates.test.ts +149 -0
  423. package/apps/sessrumnir/src/main/package-updates.ts +185 -0
  424. package/apps/sessrumnir/src/main/path-authorization.test.ts +64 -0
  425. package/apps/sessrumnir/src/main/path-authorization.ts +31 -0
  426. package/apps/sessrumnir/src/main/pi-binary-resolution.test.ts +582 -0
  427. package/apps/sessrumnir/src/main/pi-binary-resolution.ts +649 -0
  428. package/apps/sessrumnir/src/main/pi-event-router.test.ts +388 -0
  429. package/apps/sessrumnir/src/main/pi-event-router.ts +290 -0
  430. package/apps/sessrumnir/src/main/pi-paths.test.ts +134 -0
  431. package/apps/sessrumnir/src/main/pi-paths.ts +74 -0
  432. package/apps/sessrumnir/src/main/pi-rpc-manager.test.ts +467 -0
  433. package/apps/sessrumnir/src/main/pi-rpc-manager.ts +1319 -0
  434. package/apps/sessrumnir/src/main/process-tree.test.ts +94 -0
  435. package/apps/sessrumnir/src/main/renderer-origin.test.ts +33 -0
  436. package/apps/sessrumnir/src/main/renderer-origin.ts +35 -0
  437. package/apps/sessrumnir/src/main/session-lineage-reader.test.ts +317 -0
  438. package/apps/sessrumnir/src/main/session-lineage-reader.ts +111 -0
  439. package/apps/sessrumnir/src/main/session-metadata.test.ts +508 -0
  440. package/apps/sessrumnir/src/main/session-metadata.ts +526 -0
  441. package/apps/sessrumnir/src/main/session-name.test.ts +71 -0
  442. package/apps/sessrumnir/src/main/session-name.ts +69 -0
  443. package/apps/sessrumnir/src/main/session-paths.test.ts +110 -0
  444. package/apps/sessrumnir/src/main/session-paths.ts +88 -0
  445. package/apps/sessrumnir/src/main/session-tags.test.ts +61 -0
  446. package/apps/sessrumnir/src/main/session-tags.ts +253 -0
  447. package/apps/sessrumnir/src/main/session-trash.test.ts +66 -0
  448. package/apps/sessrumnir/src/main/session-trash.ts +65 -0
  449. package/apps/sessrumnir/src/main/settings-logic.ts +66 -0
  450. package/apps/sessrumnir/src/main/skills-discovery.test.ts +149 -0
  451. package/apps/sessrumnir/src/main/skills-discovery.ts +179 -0
  452. package/apps/sessrumnir/src/main/startup-launch.ts +63 -0
  453. package/apps/sessrumnir/src/main/terminal-service.ts +96 -0
  454. package/apps/sessrumnir/src/main/theme-store.test.ts +440 -0
  455. package/apps/sessrumnir/src/main/theme-store.ts +495 -0
  456. package/apps/sessrumnir/src/main/tray-decision.test.ts +46 -0
  457. package/apps/sessrumnir/src/main/tray-decision.ts +49 -0
  458. package/apps/sessrumnir/src/main/tray-manager.ts +220 -0
  459. package/apps/sessrumnir/src/main/tsconfig.json +20 -0
  460. package/apps/sessrumnir/src/main/window-background.test.ts +29 -0
  461. package/apps/sessrumnir/src/main/window-background.ts +16 -0
  462. package/apps/sessrumnir/src/main/workflow-discovery.test.ts +252 -0
  463. package/apps/sessrumnir/src/main/workflow-monitor.test.ts +371 -0
  464. package/apps/sessrumnir/src/main/workflow-monitor.ts +1020 -0
  465. package/apps/sessrumnir/src/main/workspace-activity.test.ts +227 -0
  466. package/apps/sessrumnir/src/main/workspace-activity.ts +215 -0
  467. package/apps/sessrumnir/src/main/workspace-manager.test.ts +680 -0
  468. package/apps/sessrumnir/src/main/workspace-manager.ts +1199 -0
  469. package/apps/sessrumnir/src/main/workspace-trust.test.ts +78 -0
  470. package/apps/sessrumnir/src/main/workspace-trust.ts +84 -0
  471. package/apps/sessrumnir/src/preload/index.ts +358 -0
  472. package/apps/sessrumnir/src/preload/tsconfig.json +20 -0
  473. package/apps/sessrumnir/src/renderer/index.html +18 -0
  474. package/apps/sessrumnir/src/renderer/public/theme-boot.js +30 -0
  475. package/apps/sessrumnir/src/renderer/src/app.tsx +194 -0
  476. package/apps/sessrumnir/src/renderer/src/assets/openmoji-color.woff2 +0 -0
  477. package/apps/sessrumnir/src/renderer/src/assets/pi-logo.svg +7 -0
  478. package/apps/sessrumnir/src/renderer/src/assets/ymir-mark.svg +55 -0
  479. package/apps/sessrumnir/src/renderer/src/assets.d.ts +7 -0
  480. package/apps/sessrumnir/src/renderer/src/components/chat-code-highlight.ts +239 -0
  481. package/apps/sessrumnir/src/renderer/src/components/chat-file-link.ts +140 -0
  482. package/apps/sessrumnir/src/renderer/src/components/chat-input.tsx +729 -0
  483. package/apps/sessrumnir/src/renderer/src/components/chat-panel-widths.test.ts +175 -0
  484. package/apps/sessrumnir/src/renderer/src/components/chat-panel-widths.ts +100 -0
  485. package/apps/sessrumnir/src/renderer/src/components/chat-panel.tsx +523 -0
  486. package/apps/sessrumnir/src/renderer/src/components/chat-project-picker.tsx +193 -0
  487. package/apps/sessrumnir/src/renderer/src/components/chat-search.tsx +251 -0
  488. package/apps/sessrumnir/src/renderer/src/components/code-editor-highlight.ts +85 -0
  489. package/apps/sessrumnir/src/renderer/src/components/code-editor-language.test.ts +19 -0
  490. package/apps/sessrumnir/src/renderer/src/components/code-editor-language.ts +88 -0
  491. package/apps/sessrumnir/src/renderer/src/components/code-editor.tsx +128 -0
  492. package/apps/sessrumnir/src/renderer/src/components/command-palette.tsx +315 -0
  493. package/apps/sessrumnir/src/renderer/src/components/command-results.tsx +80 -0
  494. package/apps/sessrumnir/src/renderer/src/components/composer-permission-menu.tsx +91 -0
  495. package/apps/sessrumnir/src/renderer/src/components/context-menu.tsx +457 -0
  496. package/apps/sessrumnir/src/renderer/src/components/copy-button.tsx +36 -0
  497. package/apps/sessrumnir/src/renderer/src/components/council-panels.tsx +193 -0
  498. package/apps/sessrumnir/src/renderer/src/components/custom-models-editor.tsx +330 -0
  499. package/apps/sessrumnir/src/renderer/src/components/diagnostics-panel.tsx +380 -0
  500. package/apps/sessrumnir/src/renderer/src/components/diff-viewer.tsx +318 -0
  501. package/apps/sessrumnir/src/renderer/src/components/ember-background.tsx +113 -0
  502. package/apps/sessrumnir/src/renderer/src/components/error-boundary.tsx +38 -0
  503. package/apps/sessrumnir/src/renderer/src/components/extension-ui-dialog-helpers.test.ts +43 -0
  504. package/apps/sessrumnir/src/renderer/src/components/extension-ui-dialog-helpers.ts +51 -0
  505. package/apps/sessrumnir/src/renderer/src/components/extension-ui-dialog.tsx +486 -0
  506. package/apps/sessrumnir/src/renderer/src/components/file-tree.tsx +784 -0
  507. package/apps/sessrumnir/src/renderer/src/components/git-conveyor-actions.tsx +246 -0
  508. package/apps/sessrumnir/src/renderer/src/components/home-screen.tsx +396 -0
  509. package/apps/sessrumnir/src/renderer/src/components/image-viewer.tsx +100 -0
  510. package/apps/sessrumnir/src/renderer/src/components/line-numbered-code.tsx +58 -0
  511. package/apps/sessrumnir/src/renderer/src/components/markdown-renderer.tsx +237 -0
  512. package/apps/sessrumnir/src/renderer/src/components/message-bubble.tsx +1006 -0
  513. package/apps/sessrumnir/src/renderer/src/components/mission-control.tsx +341 -0
  514. package/apps/sessrumnir/src/renderer/src/components/model-selector.tsx +219 -0
  515. package/apps/sessrumnir/src/renderer/src/components/note-picker.tsx +155 -0
  516. package/apps/sessrumnir/src/renderer/src/components/notes-panel.tsx +437 -0
  517. package/apps/sessrumnir/src/renderer/src/components/package-browser.tsx +604 -0
  518. package/apps/sessrumnir/src/renderer/src/components/permission-mode.test.ts +22 -0
  519. package/apps/sessrumnir/src/renderer/src/components/permission-mode.ts +49 -0
  520. package/apps/sessrumnir/src/renderer/src/components/permission-rules-editor-helpers.test.ts +44 -0
  521. package/apps/sessrumnir/src/renderer/src/components/permission-rules-editor-helpers.ts +35 -0
  522. package/apps/sessrumnir/src/renderer/src/components/permission-rules-editor.tsx +226 -0
  523. package/apps/sessrumnir/src/renderer/src/components/permission-selector.tsx +104 -0
  524. package/apps/sessrumnir/src/renderer/src/components/resize-handle.tsx +51 -0
  525. package/apps/sessrumnir/src/renderer/src/components/review-rail.tsx +194 -0
  526. package/apps/sessrumnir/src/renderer/src/components/session-menu-position.test.ts +22 -0
  527. package/apps/sessrumnir/src/renderer/src/components/session-menu-position.ts +34 -0
  528. package/apps/sessrumnir/src/renderer/src/components/session-panel.tsx +711 -0
  529. package/apps/sessrumnir/src/renderer/src/components/session-runtime-indicator.tsx +43 -0
  530. package/apps/sessrumnir/src/renderer/src/components/settings-panel.tsx +1360 -0
  531. package/apps/sessrumnir/src/renderer/src/components/sidebar-activity.test.ts +47 -0
  532. package/apps/sessrumnir/src/renderer/src/components/sidebar-activity.ts +76 -0
  533. package/apps/sessrumnir/src/renderer/src/components/sidebar-session-labels.test.ts +73 -0
  534. package/apps/sessrumnir/src/renderer/src/components/sidebar-session-labels.ts +47 -0
  535. package/apps/sessrumnir/src/renderer/src/components/sidebar.tsx +1045 -0
  536. package/apps/sessrumnir/src/renderer/src/components/skills-panel.tsx +159 -0
  537. package/apps/sessrumnir/src/renderer/src/components/stats-panel.tsx +368 -0
  538. package/apps/sessrumnir/src/renderer/src/components/status-bar.tsx +274 -0
  539. package/apps/sessrumnir/src/renderer/src/components/status-popover.tsx +481 -0
  540. package/apps/sessrumnir/src/renderer/src/components/streaming-bubble.tsx +128 -0
  541. package/apps/sessrumnir/src/renderer/src/components/subagent-progress.tsx +254 -0
  542. package/apps/sessrumnir/src/renderer/src/components/task-launcher.tsx +168 -0
  543. package/apps/sessrumnir/src/renderer/src/components/terminal.tsx +203 -0
  544. package/apps/sessrumnir/src/renderer/src/components/theme-editor-helpers.test.ts +35 -0
  545. package/apps/sessrumnir/src/renderer/src/components/theme-editor-helpers.ts +35 -0
  546. package/apps/sessrumnir/src/renderer/src/components/theme-editor.tsx +440 -0
  547. package/apps/sessrumnir/src/renderer/src/components/theme-gallery.tsx +373 -0
  548. package/apps/sessrumnir/src/renderer/src/components/thinking-level-selector.tsx +85 -0
  549. package/apps/sessrumnir/src/renderer/src/components/timeline.tsx +289 -0
  550. package/apps/sessrumnir/src/renderer/src/components/tool-call-icon.ts +32 -0
  551. package/apps/sessrumnir/src/renderer/src/components/workflow-navigator.tsx +901 -0
  552. package/apps/sessrumnir/src/renderer/src/components/workspace-tabs.tsx +241 -0
  553. package/apps/sessrumnir/src/renderer/src/global.d.ts +8 -0
  554. package/apps/sessrumnir/src/renderer/src/hooks/use-folder-drop.ts +101 -0
  555. package/apps/sessrumnir/src/renderer/src/hooks.test.ts +88 -0
  556. package/apps/sessrumnir/src/renderer/src/hooks.ts +574 -0
  557. package/apps/sessrumnir/src/renderer/src/i18n.test.ts +54 -0
  558. package/apps/sessrumnir/src/renderer/src/i18n.ts +89 -0
  559. package/apps/sessrumnir/src/renderer/src/index.css +425 -0
  560. package/apps/sessrumnir/src/renderer/src/main.tsx +42 -0
  561. package/apps/sessrumnir/src/renderer/src/message-grouping.test.ts +422 -0
  562. package/apps/sessrumnir/src/renderer/src/message-grouping.ts +410 -0
  563. package/apps/sessrumnir/src/renderer/src/message-parsing.test.ts +86 -0
  564. package/apps/sessrumnir/src/renderer/src/message-parsing.ts +166 -0
  565. package/apps/sessrumnir/src/renderer/src/store-external-prompts.test.ts +161 -0
  566. package/apps/sessrumnir/src/renderer/src/store-session-transitions.test.ts +2090 -0
  567. package/apps/sessrumnir/src/renderer/src/store-timeline.test.ts +52 -0
  568. package/apps/sessrumnir/src/renderer/src/store-turn-error.test.ts +136 -0
  569. package/apps/sessrumnir/src/renderer/src/store.ts +3726 -0
  570. package/apps/sessrumnir/src/renderer/src/subagent-detection.test.ts +55 -0
  571. package/apps/sessrumnir/src/renderer/src/theme/engine.test.ts +29 -0
  572. package/apps/sessrumnir/src/renderer/src/theme/engine.ts +20 -0
  573. package/apps/sessrumnir/src/renderer/src/themes/breeze-claudius.json +69 -0
  574. package/apps/sessrumnir/src/renderer/src/themes/breeze-dark.json +69 -0
  575. package/apps/sessrumnir/src/renderer/src/themes/breeze-light.json +69 -0
  576. package/apps/sessrumnir/src/renderer/src/themes/builtin-themes.test.ts +95 -0
  577. package/apps/sessrumnir/src/renderer/src/themes/dark.json +46 -0
  578. package/apps/sessrumnir/src/renderer/src/themes/fensalir.json +82 -0
  579. package/apps/sessrumnir/src/renderer/src/themes/gruvbox.json +69 -0
  580. package/apps/sessrumnir/src/renderer/src/themes/index.ts +60 -0
  581. package/apps/sessrumnir/src/renderer/src/themes/light.json +46 -0
  582. package/apps/sessrumnir/src/renderer/src/themes/nord.json +69 -0
  583. package/apps/sessrumnir/src/renderer/src/themes/sessrumnir.json +82 -0
  584. package/apps/sessrumnir/src/renderer/src/utils/debounced-buffer.test.ts +81 -0
  585. package/apps/sessrumnir/src/renderer/src/utils/debounced-buffer.ts +57 -0
  586. package/apps/sessrumnir/src/renderer/src/utils/format-relative-time.test.ts +62 -0
  587. package/apps/sessrumnir/src/renderer/src/utils/format-relative-time.ts +28 -0
  588. package/apps/sessrumnir/src/renderer/src/utils/heatmap-grid.test.ts +57 -0
  589. package/apps/sessrumnir/src/renderer/src/utils/heatmap-grid.ts +41 -0
  590. package/apps/sessrumnir/src/renderer/src/utils/ipc-error.test.ts +22 -0
  591. package/apps/sessrumnir/src/renderer/src/utils/ipc-error.ts +12 -0
  592. package/apps/sessrumnir/src/renderer/src/utils/model-search.test.ts +63 -0
  593. package/apps/sessrumnir/src/renderer/src/utils/model-search.ts +20 -0
  594. package/apps/sessrumnir/src/renderer/src/utils/planning-prompt.test.ts +31 -0
  595. package/apps/sessrumnir/src/renderer/src/utils/planning-prompt.ts +27 -0
  596. package/apps/sessrumnir/src/renderer/src/utils/preview-load-error.test.ts +15 -0
  597. package/apps/sessrumnir/src/renderer/src/utils/preview-load-error.ts +22 -0
  598. package/apps/sessrumnir/src/renderer/src/utils/process-status-label.test.ts +13 -0
  599. package/apps/sessrumnir/src/renderer/src/utils/process-status-label.ts +19 -0
  600. package/apps/sessrumnir/src/renderer/src/utils/quick-switcher.test.ts +68 -0
  601. package/apps/sessrumnir/src/renderer/src/utils/quick-switcher.ts +52 -0
  602. package/apps/sessrumnir/src/renderer/src/utils/rank-file-results.test.ts +46 -0
  603. package/apps/sessrumnir/src/renderer/src/utils/rank-file-results.ts +23 -0
  604. package/apps/sessrumnir/src/renderer/src/utils/relative-time.tsx +25 -0
  605. package/apps/sessrumnir/src/renderer/src/utils/session-title.test.ts +98 -0
  606. package/apps/sessrumnir/src/renderer/src/utils/session-title.ts +58 -0
  607. package/apps/sessrumnir/src/renderer/src/utils/stale-guard.test.ts +29 -0
  608. package/apps/sessrumnir/src/renderer/src/utils/stale-guard.ts +20 -0
  609. package/apps/sessrumnir/src/renderer/src/utils/theme.test.ts +174 -0
  610. package/apps/sessrumnir/src/renderer/src/utils/theme.ts +159 -0
  611. package/apps/sessrumnir/src/renderer/src/utils/workflow-runs.test.ts +130 -0
  612. package/apps/sessrumnir/src/renderer/src/utils/workflow-runs.ts +84 -0
  613. package/apps/sessrumnir/src/renderer/tsconfig.json +24 -0
  614. package/apps/sessrumnir/src/shared/agent-engine-label.test.ts +45 -0
  615. package/apps/sessrumnir/src/shared/agent-engine-label.ts +53 -0
  616. package/apps/sessrumnir/src/shared/app-settings.test.ts +25 -0
  617. package/apps/sessrumnir/src/shared/app-settings.ts +23 -0
  618. package/apps/sessrumnir/src/shared/bridge-http.ts +341 -0
  619. package/apps/sessrumnir/src/shared/bridge.ts +337 -0
  620. package/apps/sessrumnir/src/shared/council-config.test.ts +300 -0
  621. package/apps/sessrumnir/src/shared/council-config.ts +356 -0
  622. package/apps/sessrumnir/src/shared/default-settings.ts +39 -0
  623. package/apps/sessrumnir/src/shared/folder-drop.test.ts +118 -0
  624. package/apps/sessrumnir/src/shared/folder-drop.ts +74 -0
  625. package/apps/sessrumnir/src/shared/fork-point.test.ts +30 -0
  626. package/apps/sessrumnir/src/shared/fork-point.ts +24 -0
  627. package/apps/sessrumnir/src/shared/i18n/i18next.d.ts +10 -0
  628. package/apps/sessrumnir/src/shared/i18n/index.test.ts +51 -0
  629. package/apps/sessrumnir/src/shared/i18n/index.ts +50 -0
  630. package/apps/sessrumnir/src/shared/i18n/languages.ts +12 -0
  631. package/apps/sessrumnir/src/shared/i18n/locale-checks.ts +98 -0
  632. package/apps/sessrumnir/src/shared/i18n/locales.test.ts +92 -0
  633. package/apps/sessrumnir/src/shared/i18n/pseudo.test.ts +39 -0
  634. package/apps/sessrumnir/src/shared/i18n/pseudo.ts +44 -0
  635. package/apps/sessrumnir/src/shared/i18n/resolve.test.ts +43 -0
  636. package/apps/sessrumnir/src/shared/i18n/resolve.ts +41 -0
  637. package/apps/sessrumnir/src/shared/i18n/resources.ts +11 -0
  638. package/apps/sessrumnir/src/shared/ipc-contracts.ts +1539 -0
  639. package/apps/sessrumnir/src/shared/models-config.test.ts +124 -0
  640. package/apps/sessrumnir/src/shared/models-config.ts +167 -0
  641. package/apps/sessrumnir/src/shared/package-filter.ts +19 -0
  642. package/apps/sessrumnir/src/shared/package-spec.test.ts +51 -0
  643. package/apps/sessrumnir/src/shared/package-spec.ts +25 -0
  644. package/apps/sessrumnir/src/shared/path-compare.test.ts +38 -0
  645. package/apps/sessrumnir/src/shared/path-compare.ts +76 -0
  646. package/apps/sessrumnir/src/shared/permission-mode.ts +8 -0
  647. package/apps/sessrumnir/src/shared/pi-command.test.ts +165 -0
  648. package/apps/sessrumnir/src/shared/pi-command.ts +139 -0
  649. package/apps/sessrumnir/src/shared/product-name.ts +7 -0
  650. package/apps/sessrumnir/src/shared/session-lineage.test.ts +103 -0
  651. package/apps/sessrumnir/src/shared/session-lineage.ts +50 -0
  652. package/apps/sessrumnir/src/shared/session-preview.test.ts +93 -0
  653. package/apps/sessrumnir/src/shared/session-preview.ts +56 -0
  654. package/apps/sessrumnir/src/shared/sidebar-width.test.ts +58 -0
  655. package/apps/sessrumnir/src/shared/sidebar-width.ts +37 -0
  656. package/apps/sessrumnir/src/shared/theme/builtin-ids.ts +16 -0
  657. package/apps/sessrumnir/src/shared/theme/resolve.test.ts +66 -0
  658. package/apps/sessrumnir/src/shared/theme/resolve.ts +29 -0
  659. package/apps/sessrumnir/src/shared/theme/syntax-defaults.ts +24 -0
  660. package/apps/sessrumnir/src/shared/theme/theme-file.test.ts +88 -0
  661. package/apps/sessrumnir/src/shared/theme/theme-file.ts +132 -0
  662. package/apps/sessrumnir/src/shared/theme/tokens.test.ts +35 -0
  663. package/apps/sessrumnir/src/shared/theme/tokens.ts +86 -0
  664. package/apps/sessrumnir/src/shared/tsconfig.json +17 -0
  665. package/apps/sessrumnir/src/shared/untrusted-data.test.ts +32 -0
  666. package/apps/sessrumnir/src/shared/untrusted-data.ts +22 -0
  667. package/apps/sessrumnir/src/shared/version-compare.test.ts +33 -0
  668. package/apps/sessrumnir/src/shared/version-compare.ts +50 -0
  669. package/apps/sessrumnir/src/shared/workflow-control.ts +33 -0
  670. package/apps/sessrumnir/src/web-server.ts +315 -0
  671. package/apps/sessrumnir/tsconfig.json +26 -0
  672. package/apps/smidja/smidja_build.py +45 -0
  673. package/apps/smidja/smidja_build_review.py +76 -0
  674. package/apps/smidja/smidja_build_test.py +79 -0
  675. package/apps/smidja/smidja_document.py +76 -0
  676. package/apps/smidja/smidja_modules/agent_cc.py +15 -0
  677. package/apps/smidja/smidja_modules/agent_opencode.py +194 -0
  678. package/apps/smidja/smidja_modules/agent_pi.py +392 -0
  679. package/apps/smidja/smidja_modules/agents.py +993 -0
  680. package/apps/smidja/smidja_modules/changes.py +103 -0
  681. package/apps/smidja/smidja_modules/console.py +131 -0
  682. package/apps/smidja/smidja_modules/data_types.py +447 -0
  683. package/apps/smidja/smidja_modules/gates.py +108 -0
  684. package/apps/smidja/smidja_modules/git_helper.py +120 -0
  685. package/apps/smidja/smidja_modules/lm_local.py +144 -0
  686. package/apps/smidja/smidja_modules/permissions.py +194 -0
  687. package/apps/smidja/smidja_modules/prompts.py +21 -0
  688. package/apps/smidja/smidja_modules/quality.py +240 -0
  689. package/apps/smidja/smidja_modules/runner.py +155 -0
  690. package/apps/smidja/smidja_modules/session.py +50 -0
  691. package/apps/smidja/smidja_modules/stall_watch.py +149 -0
  692. package/apps/smidja/smidja_modules/tracer.py +271 -0
  693. package/apps/smidja/smidja_modules/utils.py +77 -0
  694. package/apps/smidja/smidja_orchestrate.py +162 -0
  695. package/apps/smidja/smidja_plan.py +45 -0
  696. package/apps/smidja/smidja_plan_build.py +55 -0
  697. package/apps/smidja/smidja_plan_build_test.py +86 -0
  698. package/apps/smidja/smidja_plan_build_test_quality.py +98 -0
  699. package/apps/smidja/smidja_prompt.py +44 -0
  700. package/apps/smidja/smidja_quality.py +49 -0
  701. package/apps/smidja/smidja_recon_iv.py +202 -0
  702. package/apps/smidja/smidja_scout.py +45 -0
  703. package/apps/smidja/smidja_simple_sdlc.py +183 -0
  704. package/apps/smidja/smidja_smidja_config/smidja.config.yaml +143 -0
  705. package/apps/smidja-factory/AGENTS.md +165 -0
  706. package/apps/smidja-factory/README.md +3 -0
  707. package/apps/smidja-factory/SKILL.md +18 -0
  708. package/apps/smidja-factory/apps/visualizer/.oxlintrc.json +18 -0
  709. package/apps/smidja-factory/apps/visualizer/bun.lock +269 -0
  710. package/apps/smidja-factory/apps/visualizer/desktop/icon.png +0 -0
  711. package/apps/smidja-factory/apps/visualizer/desktop/main.js +140 -0
  712. package/apps/smidja-factory/apps/visualizer/desktop/package-lock.json +871 -0
  713. package/apps/smidja-factory/apps/visualizer/desktop/package.json +18 -0
  714. package/apps/smidja-factory/apps/visualizer/desktop/preload.js +19 -0
  715. package/apps/smidja-factory/apps/visualizer/index.html +28 -0
  716. package/apps/smidja-factory/apps/visualizer/package.json +31 -0
  717. package/apps/smidja-factory/apps/visualizer/public/icon.svg +7 -0
  718. package/apps/smidja-factory/apps/visualizer/public/models/claude.png +0 -0
  719. package/apps/smidja-factory/apps/visualizer/public/models/gemini.png +0 -0
  720. package/apps/smidja-factory/apps/visualizer/public/models/kimi.png +0 -0
  721. package/apps/smidja-factory/apps/visualizer/public/models/openai.png +0 -0
  722. package/apps/smidja-factory/apps/visualizer/public/models/zai.png +0 -0
  723. package/apps/smidja-factory/apps/visualizer/server/chat.ts +425 -0
  724. package/apps/smidja-factory/apps/visualizer/server/db.ts +1050 -0
  725. package/apps/smidja-factory/apps/visualizer/server/index.ts +648 -0
  726. package/apps/smidja-factory/apps/visualizer/server/model-catalog.ts +90 -0
  727. package/apps/smidja-factory/apps/visualizer/server/roster-api.ts +75 -0
  728. package/apps/smidja-factory/apps/visualizer/server/roster_resolve.py +170 -0
  729. package/apps/smidja-factory/apps/visualizer/server/settings.ts +106 -0
  730. package/apps/smidja-factory/apps/visualizer/shared/types.ts +630 -0
  731. package/apps/smidja-factory/apps/visualizer/src/App.vue +379 -0
  732. package/apps/smidja-factory/apps/visualizer/src/components/ChatInput.vue +155 -0
  733. package/apps/smidja-factory/apps/visualizer/src/components/ChatThread.vue +219 -0
  734. package/apps/smidja-factory/apps/visualizer/src/components/ChatView.vue +335 -0
  735. package/apps/smidja-factory/apps/visualizer/src/components/DecisionsView.vue +177 -0
  736. package/apps/smidja-factory/apps/visualizer/src/components/DetailSection.vue +76 -0
  737. package/apps/smidja-factory/apps/visualizer/src/components/MemoryView.vue +397 -0
  738. package/apps/smidja-factory/apps/visualizer/src/components/PhaseDetail.vue +1354 -0
  739. package/apps/smidja-factory/apps/visualizer/src/components/PhaseDots.vue +55 -0
  740. package/apps/smidja-factory/apps/visualizer/src/components/SessionCard.vue +583 -0
  741. package/apps/smidja-factory/apps/visualizer/src/components/SessionMiniCard.vue +58 -0
  742. package/apps/smidja-factory/apps/visualizer/src/components/SessionPanel.vue +272 -0
  743. package/apps/smidja-factory/apps/visualizer/src/components/SessionStartForm.vue +152 -0
  744. package/apps/smidja-factory/apps/visualizer/src/components/SessionTrace.vue +1157 -0
  745. package/apps/smidja-factory/apps/visualizer/src/components/SessionsList.vue +96 -0
  746. package/apps/smidja-factory/apps/visualizer/src/components/SettingsView.vue +152 -0
  747. package/apps/smidja-factory/apps/visualizer/src/components/StatChip.vue +98 -0
  748. package/apps/smidja-factory/apps/visualizer/src/components/StatsView.vue +382 -0
  749. package/apps/smidja-factory/apps/visualizer/src/components/StatusChip.vue +73 -0
  750. package/apps/smidja-factory/apps/visualizer/src/components/ToolCallCard.vue +156 -0
  751. package/apps/smidja-factory/apps/visualizer/src/lib/api.ts +177 -0
  752. package/apps/smidja-factory/apps/visualizer/src/lib/chat-api.ts +95 -0
  753. package/apps/smidja-factory/apps/visualizer/src/lib/chat-store.ts +351 -0
  754. package/apps/smidja-factory/apps/visualizer/src/lib/events.ts +131 -0
  755. package/apps/smidja-factory/apps/visualizer/src/lib/format.ts +92 -0
  756. package/apps/smidja-factory/apps/visualizer/src/lib/highlight.ts +54 -0
  757. package/apps/smidja-factory/apps/visualizer/src/lib/markdown.ts +120 -0
  758. package/apps/smidja-factory/apps/visualizer/src/lib/models.ts +42 -0
  759. package/apps/smidja-factory/apps/visualizer/src/lib/router.ts +67 -0
  760. package/apps/smidja-factory/apps/visualizer/src/lib/types.ts +61 -0
  761. package/apps/smidja-factory/apps/visualizer/src/main.ts +13 -0
  762. package/apps/smidja-factory/apps/visualizer/src/style.css +592 -0
  763. package/apps/smidja-factory/apps/visualizer/tsconfig.json +30 -0
  764. package/apps/smidja-factory/apps/visualizer/vite.config.js +32 -0
  765. package/apps/smidja-factory/apps/visualizer/vite.config.ts +31 -0
  766. package/apps/smidja-factory/cookbooks/create_config.md +61 -0
  767. package/apps/smidja-factory/cookbooks/create_smidja.md +120 -0
  768. package/apps/smidja-factory/cookbooks/how_to_prompt_for_the_eng.md +110 -0
  769. package/apps/smidja-factory/cookbooks/install.md +56 -0
  770. package/apps/smidja-factory/cookbooks/run_smidja.md +122 -0
  771. package/apps/smidja-factory/cookbooks/smidja_overview.md +88 -0
  772. package/apps/smidja-factory/cookbooks/update_config.md +103 -0
  773. package/apps/smidja-factory/cookbooks/update_modules.md +100 -0
  774. package/apps/smidja-factory/cookbooks/update_smidja.md +90 -0
  775. package/apps/smidja-factory/docs/enhancement-plan.md +420 -0
  776. package/apps/smidja-factory/references/config.md +202 -0
  777. package/apps/smidja-factory/references/handoff.md +161 -0
  778. package/apps/smidja-factory/references/observability.md +166 -0
  779. package/apps/smidja-factory/scripts/install.py +113 -0
  780. package/apps/smidja-factory/scripts/make_config.py +35 -0
  781. package/apps/smidja-factory/scripts/make_smidja.py +119 -0
  782. package/apps/smidja-factory/skills/add-or-edit-ai-models/SKILL.md +151 -0
  783. package/apps/smidja-factory/skills/create-new-agent/SKILL.md +125 -0
  784. package/apps/smidja-factory/skills/create-new-teams/SKILL.md +151 -0
  785. package/apps/smidja-factory/skills/git-ops/README.md +25 -0
  786. package/apps/smidja-factory/skills/git-ops/SKILL.md +18 -0
  787. package/apps/smidja-factory/skills/git-ops/create_branch.sh +79 -0
  788. package/apps/smidja-factory/skills/git-ops/safe_commit.sh +82 -0
  789. package/apps/smidja-factory/skills/git-ops/sync_upstream.sh +94 -0
  790. package/apps/smidja-factory/skills/how-to-run-agent-teams/SKILL.md +117 -0
  791. package/apps/smidja-factory/skills/smidja-instructions/SKILL.md +168 -0
  792. package/apps/smidja-factory/skills/smidja-launcher/SKILL.md +155 -0
  793. package/apps/smidja-factory/skills/smidja-start/SKILL.md +266 -0
  794. package/apps/smidja-factory/skills/volundr/SKILL.md +232 -0
  795. package/apps/smidja-factory/skills/volundr/prompt/dispatch-prompt.md +91 -0
  796. package/apps/smidja-factory/skills/volundr/prompt/volundr-system.md +63 -0
  797. package/apps/smidja-factory/skills/volundr/scripts/volundr-observe.py +52 -0
  798. package/apps/smidja-factory/skills/volundr/scripts/volundr-recall.py +54 -0
  799. package/apps/smidja-factory/skills/volundr/scripts/volundr-status.py +51 -0
  800. package/apps/smidja-factory/skills/volundr/scripts/volundr-teach.py +47 -0
  801. package/apps/smidja-factory/templates/env.sample +28 -0
  802. package/apps/smidja-factory/templates/harness_engineering/subagents.ts +706 -0
  803. package/apps/smidja-factory/templates/harness_engineering/themeMap.ts +145 -0
  804. package/apps/smidja-factory/templates/justfile +89 -0
  805. package/apps/smidja-factory/templates/prompt_engineering/builder/system.md +13 -0
  806. package/apps/smidja-factory/templates/prompt_engineering/builder/user.md +34 -0
  807. package/apps/smidja-factory/templates/prompt_engineering/documenter/system.md +17 -0
  808. package/apps/smidja-factory/templates/prompt_engineering/documenter/user.md +48 -0
  809. package/apps/smidja-factory/templates/prompt_engineering/orchestrator/system.md +55 -0
  810. package/apps/smidja-factory/templates/prompt_engineering/orchestrator/user.md +39 -0
  811. package/apps/smidja-factory/templates/prompt_engineering/planner/system.md +21 -0
  812. package/apps/smidja-factory/templates/prompt_engineering/planner/user.md +45 -0
  813. package/apps/smidja-factory/templates/prompt_engineering/recon_orchestrator/system.md +51 -0
  814. package/apps/smidja-factory/templates/prompt_engineering/recon_orchestrator/user.md +26 -0
  815. package/apps/smidja-factory/templates/prompt_engineering/reviewer/system.md +16 -0
  816. package/apps/smidja-factory/templates/prompt_engineering/reviewer/user.md +44 -0
  817. package/apps/smidja-factory/templates/prompt_engineering/scout/system.md +32 -0
  818. package/apps/smidja-factory/templates/prompt_engineering/scout/user.md +34 -0
  819. package/apps/smidja-factory/templates/smidja/smidja_build.py +45 -0
  820. package/apps/smidja-factory/templates/smidja/smidja_build_review.py +76 -0
  821. package/apps/smidja-factory/templates/smidja/smidja_build_test.py +79 -0
  822. package/apps/smidja-factory/templates/smidja/smidja_document.py +76 -0
  823. package/apps/smidja-factory/templates/smidja/smidja_modules/agent_cc.py +15 -0
  824. package/apps/smidja-factory/templates/smidja/smidja_modules/agent_opencode.py +194 -0
  825. package/apps/smidja-factory/templates/smidja/smidja_modules/agent_pi.py +392 -0
  826. package/apps/smidja-factory/templates/smidja/smidja_modules/agents.py +993 -0
  827. package/apps/smidja-factory/templates/smidja/smidja_modules/changes.py +103 -0
  828. package/apps/smidja-factory/templates/smidja/smidja_modules/console.py +131 -0
  829. package/apps/smidja-factory/templates/smidja/smidja_modules/data_types.py +447 -0
  830. package/apps/smidja-factory/templates/smidja/smidja_modules/gates.py +108 -0
  831. package/apps/smidja-factory/templates/smidja/smidja_modules/git_helper.py +120 -0
  832. package/apps/smidja-factory/templates/smidja/smidja_modules/lm_local.py +144 -0
  833. package/apps/smidja-factory/templates/smidja/smidja_modules/permissions.py +194 -0
  834. package/apps/smidja-factory/templates/smidja/smidja_modules/prompts.py +21 -0
  835. package/apps/smidja-factory/templates/smidja/smidja_modules/quality.py +240 -0
  836. package/apps/smidja-factory/templates/smidja/smidja_modules/runner.py +155 -0
  837. package/apps/smidja-factory/templates/smidja/smidja_modules/session.py +50 -0
  838. package/apps/smidja-factory/templates/smidja/smidja_modules/stall_watch.py +149 -0
  839. package/apps/smidja-factory/templates/smidja/smidja_modules/tracer.py +271 -0
  840. package/apps/smidja-factory/templates/smidja/smidja_modules/utils.py +77 -0
  841. package/apps/smidja-factory/templates/smidja/smidja_orchestrate.py +162 -0
  842. package/apps/smidja-factory/templates/smidja/smidja_plan.py +45 -0
  843. package/apps/smidja-factory/templates/smidja/smidja_plan_build.py +55 -0
  844. package/apps/smidja-factory/templates/smidja/smidja_plan_build_test.py +86 -0
  845. package/apps/smidja-factory/templates/smidja/smidja_plan_build_test_quality.py +98 -0
  846. package/apps/smidja-factory/templates/smidja/smidja_prompt.py +44 -0
  847. package/apps/smidja-factory/templates/smidja/smidja_quality.py +49 -0
  848. package/apps/smidja-factory/templates/smidja/smidja_recon_iv.py +202 -0
  849. package/apps/smidja-factory/templates/smidja/smidja_scout.py +45 -0
  850. package/apps/smidja-factory/templates/smidja/smidja_simple_sdlc.py +183 -0
  851. package/apps/smidja-factory/templates/smidja.config.yaml +143 -0
  852. package/bin/app-build.sh +36 -0
  853. package/bin/electron-lib.sh +25 -0
  854. package/bin/sessrumnir.sh +1 -1
  855. package/bin/ymir-install.sh +7 -0
  856. package/package.json +15 -7
  857. package/scripts/electron.sh +6 -2
  858. package/.agents/bus/messages.json +0 -1
  859. package/.agents/config/agents.machine.example.yaml +0 -16
  860. package/.pi/mcp.json +0 -16
  861. package/assets/reference/state/.branch-eligible-owner +0 -4
  862. package/assets/reference/state/.branch-mirror-cursor +0 -1
  863. package/assets/reference/state/.branch-outcomes-cursor +0 -1
  864. package/assets/reference/state/.branch-session +0 -1
  865. package/assets/reference/state/.count-default_wF_p5 +0 -1
  866. package/assets/reference/state/.count-default_wF_p6 +0 -1
  867. package/assets/reference/state/.count-default_wR_p2 +0 -1
  868. package/assets/reference/state/.count-default_wS_p2 +0 -1
  869. package/assets/reference/state/.count-firstmate_fm-server-knowledge +0 -1
  870. package/assets/reference/state/.hash-default_wF_p5 +0 -1
  871. package/assets/reference/state/.hash-default_wF_p6 +0 -1
  872. package/assets/reference/state/.hash-default_wR_p2 +0 -1
  873. package/assets/reference/state/.hash-default_wS_p2 +0 -1
  874. package/assets/reference/state/.hash-firstmate_fm-server-knowledge +0 -1
  875. package/assets/reference/state/.heartbeat-streak +0 -1
  876. package/assets/reference/state/.inactive-outcome-reconcile +0 -2
  877. package/assets/reference/state/.lock +0 -1
  878. package/assets/reference/state/.pi-branch-extension-loaded +0 -1
  879. package/assets/reference/state/.pi-turnend-extension-loaded +0 -2
  880. package/assets/reference/state/.pi-watch-extension-loaded +0 -2
  881. package/assets/reference/state/.seen-server-knowledge_turn-ended +0 -1
  882. package/assets/reference/state/.session-start-agents-baseline +0 -2
  883. package/assets/reference/state/.session-start-complete +0 -1
  884. package/assets/reference/state/.stale-default_wF_p5 +0 -1
  885. package/assets/reference/state/.stale-default_wR_p2 +0 -1
  886. package/assets/reference/state/.stale-firstmate_fm-server-knowledge +0 -1
  887. package/assets/reference/state/.stale-since-default_wF_p5 +0 -1
  888. package/assets/reference/state/.stale-since-default_wR_p2 +0 -1
  889. package/assets/reference/state/.stale-since-firstmate_fm-server-knowledge +0 -1
  890. package/assets/reference/state/.startup-network.delivered +0 -1
  891. package/assets/reference/state/.startup-network.report +0 -1
  892. package/assets/reference/state/.startup-network.status +0 -10
  893. package/assets/reference/state/.startup-network.timings +0 -6
  894. package/assets/reference/state/.trace-context-effective +0 -1
  895. package/assets/reference/state/.wake-queue +0 -0
  896. package/assets/reference/state/.wake-queue.seq +0 -1
  897. package/assets/reference/state/.wake-rows.consume.BcqXSq +0 -0
  898. package/assets/reference/state/.wake-rows.consume.NDZZei +0 -0
  899. package/assets/reference/state/.wake-rows.consume.VGkhYm +0 -0
  900. package/assets/reference/state/.wake-rows.consume.tR2rTy +0 -0
  901. package/assets/reference/state/.watch-arm-output.3581p6 +0 -0
  902. package/assets/reference/state/.watch-arm-output.3ciDNP +0 -0
  903. package/assets/reference/state/.watch-arm-output.SksR6k +0 -0
  904. package/assets/reference/state/.watch-cycle-exits.log +0 -22
  905. package/assets/reference/state/.watch-deliveries.log +0 -19
  906. package/assets/reference/state/.watch-triage.log +0 -38
  907. package/assets/reference/state/.watch.lock/fm-home +0 -1
  908. package/assets/reference/state/.watch.lock/pid +0 -1
  909. package/assets/reference/state/.watch.lock/pid-identity +0 -1
  910. package/assets/reference/state/.watch.lock/watcher-path +0 -1
  911. package/assets/reference/state/.watch.lock.owner.qqbxmS/fm-home +0 -1
  912. package/assets/reference/state/.watch.lock.owner.qqbxmS/pid +0 -1
  913. package/assets/reference/state/.watch.lock.owner.qqbxmS/pid-identity +0 -1
  914. package/assets/reference/state/.watch.lock.owner.qqbxmS/watcher-path +0 -1
  915. package/assets/reference/state/.watcher-down +0 -1
  916. package/assets/reference/state/branch-outcomes.jsonl +0 -1
  917. package/assets/reference/state/branch-session/2026-09-04T16-49-38-671Z_01a06d53-6b6f-7619-930d-e6dfd9a4a29e.jsonl +0 -211
  918. package/assets/reference/state/home-summary.json +0 -42
  919. package/assets/reference/state/server-knowledge.busy-gen +0 -1
  920. package/assets/reference/state/server-knowledge.busy-state +0 -1
  921. package/assets/reference/state/server-knowledge.herdr-presentation +0 -12
  922. package/assets/reference/state/server-knowledge.meta +0 -18
  923. package/assets/reference/state/server-knowledge.pi-ext.ts +0 -26
  924. package/assets/reference/state/server-knowledge.turn-ended +0 -0
  925. /package/{assets/reference/state/.last-check → apps/hlidskjalf/src/app/.gitkeep} +0 -0
  926. /package/{assets/reference/state/.last-heartbeat → apps/hlidskjalf/src/components/.gitkeep} +0 -0
  927. /package/{assets/reference/state/.last-watcher-beat → apps/hlidskjalf/src/services/.gitkeep} +0 -0
  928. /package/{assets/reference/state/.main-eligible-rows.tmp.p2HMKo → apps/smidja/smidja_modules/__init__.py} +0 -0
  929. /package/{assets/reference/state/.main-eligible-rows.tmp.Exxm82 → apps/smidja-factory/templates/smidja/smidja_modules/__init__.py} +0 -0
@@ -0,0 +1,1199 @@
1
+ import { dirname, basename, resolve } from 'path'
2
+ import { readFile, writeFile, mkdir, rename, copyFile } from 'fs/promises'
3
+ import { existsSync } from 'fs'
4
+ import { PiRpcManager } from './pi-rpc-manager'
5
+ import { FileService } from './file-service'
6
+ import type {
7
+ FileChangeEvent,
8
+ PiStartOptions,
9
+ WorkspaceRemoveResult,
10
+ WorkspaceTabOptions,
11
+ SessionRuntimeInfo,
12
+ SessionRuntimeActivity,
13
+ SessionRuntimeCloseResult,
14
+ } from '../shared/ipc-contracts'
15
+ import { getGuiDataPath } from './app-data-paths'
16
+ import { engineForBoundSession, isWithinSessionRoots } from './pi-paths'
17
+ import { pathsEqual, pathGroupKey } from './session-paths'
18
+ import { isPathWithin } from './path-authorization'
19
+ import { appLog } from './app-log'
20
+ import { inspectSessionContent } from './session-metadata'
21
+ import {
22
+ createGitWorktree,
23
+ describeGitFailure,
24
+ GitCommandError,
25
+ inspectGitRepository,
26
+ listGitWorktrees,
27
+ removeGitWorktree,
28
+ slugifyWorktreePart,
29
+ worktreeBranchName,
30
+ worktreeTargetPath,
31
+ } from './git-worktree'
32
+ import { extractGitHubPullRequestUrl, resolvePullRequestHeadBranch } from './git-conveyor'
33
+ import { t, tEnglish } from '../shared/i18n'
34
+
35
+ /**
36
+ * Manages project workspaces and their independent Pi session runtimes.
37
+ * Multiple runtimes may share one workspace directory; the workspace list is
38
+ * persisted, while live runtime processes are intentionally in-memory.
39
+ *
40
+ * Persistence: workspace list stored in the Electron userData directory.
41
+ */
42
+
43
+ const WORKSPACES_FILE = 'workspaces.json'
44
+
45
+ /**
46
+ * Directory under the GUI data path holding every worktree this app creates.
47
+ * It is also the only place a task may reuse a checkout from: everything
48
+ * outside it — the user's clone above all — belongs to the user.
49
+ */
50
+ const MANAGED_WORKTREES_DIR = 'worktrees'
51
+
52
+ /**
53
+ * How many session runtimes may own a live Pi child process at the same time.
54
+ * Every runtime is a full agent process, so without a bound the app leaks one
55
+ * process per session the user ever opens, until it quits. The active session
56
+ * plus a small recently-used set keeps tab switching instant while capping
57
+ * memory; an evicted tab stays open and spawns a fresh process when selected
58
+ * again, because its session lives on disk and not in the process.
59
+ */
60
+ export const MAX_LIVE_SESSION_RUNTIMES = 6
61
+
62
+ export interface Workspace {
63
+ id: string
64
+ name: string
65
+ path: string
66
+ createdAt: number
67
+ lastActiveAt: number
68
+ color: string
69
+ /** Optional on disk for backward compatibility with older workspace files. */
70
+ kind?: 'folder' | 'worktree'
71
+ repoRoot?: string
72
+ branch?: string
73
+ baseRef?: string
74
+ sourceWasDirty?: boolean
75
+ /** False for an existing user worktree adopted by the app; never delete it on close. */
76
+ managed?: boolean
77
+ /** Original task text when the app created or adopted this worktree. */
78
+ taskPrompt?: string
79
+ }
80
+
81
+ interface WorkspaceState {
82
+ workspaces: Workspace[]
83
+ activeWorkspaceId: string | null
84
+ }
85
+
86
+ const WORKSPACE_COLORS = [
87
+ '#3b82f6', '#ef4444', '#22c55e', '#eab308', '#a855f7',
88
+ '#ec4899', '#06b6d4', '#f97316', '#6366f1', '#14b8a6',
89
+ ]
90
+
91
+ export type PiManagerListener = (manager: PiRpcManager) => void
92
+ export type ActiveWorkspaceListener = (workspaceId: string | null) => void
93
+ export type FileChangeListener = (event: FileChangeEvent) => void
94
+ export type WorkspaceRemovedListener = (workspaceId: string) => void
95
+ export type SessionRuntimeListener = (runtime: SessionRuntimeInfo) => void
96
+
97
+ interface SessionRuntimeEntry {
98
+ info: SessionRuntimeInfo
99
+ manager: PiRpcManager
100
+ /**
101
+ * Wall clock of the last time this runtime was used (created, activated,
102
+ * started, commanded, or changed activity). Drives least-recently-active
103
+ * eviction; kept off SessionRuntimeInfo because the renderer never needs it.
104
+ */
105
+ lastActiveAt: number
106
+ /**
107
+ * True while every session file this runtime has ever pointed at was written
108
+ * by the child process we spawned for it. Set when the tab is opened with no
109
+ * session, cleared the moment a start binds it to a file that already exists
110
+ * on disk. It is the one thing that makes a close-time delete safe — see
111
+ * isDisposableSessionFile.
112
+ */
113
+ appCreated: boolean
114
+ }
115
+
116
+ /**
117
+ * The ONLY condition under which this app deletes a session file the user never
118
+ * asked it to delete. Every clause has to hold:
119
+ *
120
+ * - `appCreated`: the file was written by an agent this app spawned during
121
+ * this run, for a tab that started with no session. A file that already
122
+ * existed — opened from the list, forked, or resumed with --continue — is
123
+ * the user's conversation and is never auto-deleted, whatever it looks like
124
+ * on disk.
125
+ * - `empty`: the close-time read PROVED it header-only. `unknown` (an
126
+ * unreadable or partial read) is not empty and never reaches here.
127
+ * - inside a session store, and still present.
128
+ *
129
+ * Deletion is permanent when `trash` is missing, so reading as empty is on its
130
+ * own never a reason to remove anything: leaving a junk file behind costs
131
+ * nothing, losing a conversation cannot be undone.
132
+ */
133
+ export function isDisposableSessionFile(
134
+ result: SessionRuntimeCloseResult
135
+ ): result is SessionRuntimeCloseResult & { sessionPath: string } {
136
+ return (
137
+ result.appCreated &&
138
+ result.empty &&
139
+ result.sessionPath !== null &&
140
+ isWithinSessionRoots(result.sessionPath) &&
141
+ existsSync(result.sessionPath)
142
+ )
143
+ }
144
+
145
+ export class WorkspaceManager {
146
+ private workspaces: Workspace[] = []
147
+ private activeWorkspaceId: string | null = null
148
+ private piManagers = new Map<string, PiRpcManager>()
149
+ private fileServices = new Map<string, FileService>()
150
+ // A workspace is a project container. Each live session gets its own Pi
151
+ // process, even when several sessions share the same workspace cwd.
152
+ private sessionRuntimes = new Map<string, SessionRuntimeEntry>()
153
+ private runtimeBySessionPath = new Map<string, string>()
154
+ private activeRuntimeByWorkspace = new Map<string, string>()
155
+ private activeRuntimeId: string | null = null
156
+ private sessionRuntimeListeners: SessionRuntimeListener[] = []
157
+ private configPath: string
158
+ private nextColorIndex = 0
159
+ private piManagerListeners: PiManagerListener[] = []
160
+ // Track which (manager, listener) pairs are already wired so we never call
161
+ // the same listener twice for the same manager. Using a WeakSet keyed on
162
+ // the manager alone (the old design) was buggy: a manager that was created
163
+ // BEFORE any listeners were registered would be marked "wired" and never
164
+ // get the listeners that arrived later — silently dropping every Pi event
165
+ // for managers loaded from disk during `initialize()`.
166
+ private wiredPairs = new WeakMap<PiRpcManager, Set<PiManagerListener>>()
167
+ private activeWorkspaceListeners: ActiveWorkspaceListener[] = []
168
+ private fileChangeListeners: FileChangeListener[] = []
169
+ private workspaceRemovedListeners: WorkspaceRemovedListener[] = []
170
+ // The workspace whose FileService currently has an active disk watcher.
171
+ // Only the active workspace is watched, mirroring how Pi events are
172
+ // forwarded for the active workspace only.
173
+ private watchingWorkspaceId: string | null = null
174
+
175
+ constructor() {
176
+ this.configPath = getGuiDataPath(WORKSPACES_FILE)
177
+ }
178
+
179
+ onFileChange(listener: FileChangeListener): void {
180
+ this.fileChangeListeners.push(listener)
181
+ }
182
+
183
+ onWorkspaceRemoved(listener: WorkspaceRemovedListener): void {
184
+ this.workspaceRemovedListeners.push(listener)
185
+ }
186
+
187
+ private emitFileChange(event: FileChangeEvent): void {
188
+ for (const listener of this.fileChangeListeners) {
189
+ listener(event)
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Ensure the disk watcher is attached to the active workspace's FileService
195
+ * (and detached from any previously-watched one). Called on startup and on
196
+ * every active-workspace change.
197
+ */
198
+ private updateActiveWatcher(): void {
199
+ if (this.watchingWorkspaceId === this.activeWorkspaceId) return
200
+
201
+ if (this.watchingWorkspaceId) {
202
+ this.fileServices.get(this.watchingWorkspaceId)?.stopWatching()
203
+ }
204
+
205
+ this.watchingWorkspaceId = this.activeWorkspaceId
206
+ if (this.activeWorkspaceId) {
207
+ this.fileServices
208
+ .get(this.activeWorkspaceId)
209
+ ?.startWatching((event) => this.emitFileChange(event))
210
+ }
211
+ }
212
+
213
+ onPiManager(listener: PiManagerListener): void {
214
+ this.piManagerListeners.push(listener)
215
+ // Attach this NEW listener to every existing manager (subject to the
216
+ // per-pair dedup below). This is what makes late-registered listeners
217
+ // (e.g. the IPC broadcaster, which registers after workspaces have been
218
+ // loaded from disk) actually receive events.
219
+ for (const manager of this.piManagers.values()) {
220
+ this.attachListenerOnce(manager, listener)
221
+ }
222
+ for (const entry of this.sessionRuntimes.values()) {
223
+ this.attachListenerOnce(entry.manager, listener)
224
+ }
225
+ }
226
+
227
+ onActiveWorkspaceChanged(listener: ActiveWorkspaceListener): void {
228
+ this.activeWorkspaceListeners.push(listener)
229
+ }
230
+
231
+ private emitActiveWorkspaceChanged(): void {
232
+ this.updateActiveWatcher()
233
+ for (const listener of this.activeWorkspaceListeners) {
234
+ listener(this.activeWorkspaceId)
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Wire all currently-registered listeners to a manager (called when a
240
+ * new manager is created). Per-pair dedup ensures a listener doesn't
241
+ * get attached twice if `wirePiManager` is called more than once for
242
+ * the same manager (e.g. createWorkspace + later startPiForWorkspace).
243
+ */
244
+ private wirePiManager(manager: PiRpcManager): void {
245
+ for (const listener of this.piManagerListeners) {
246
+ this.attachListenerOnce(manager, listener)
247
+ }
248
+ }
249
+
250
+ private attachListenerOnce(manager: PiRpcManager, listener: PiManagerListener): void {
251
+ let attached = this.wiredPairs.get(manager)
252
+ if (!attached) {
253
+ attached = new Set()
254
+ this.wiredPairs.set(manager, attached)
255
+ }
256
+ if (attached.has(listener)) return
257
+ attached.add(listener)
258
+ listener(manager)
259
+ }
260
+
261
+ onSessionRuntime(listener: SessionRuntimeListener): void {
262
+ this.sessionRuntimeListeners.push(listener)
263
+ for (const entry of this.sessionRuntimes.values()) listener(this.snapshotRuntime(entry))
264
+ }
265
+
266
+ private snapshotRuntime(entry: SessionRuntimeEntry): SessionRuntimeInfo {
267
+ return {
268
+ ...entry.info,
269
+ ...entry.manager.getStatus(),
270
+ active: entry.info.runtimeId === this.activeRuntimeId,
271
+ }
272
+ }
273
+
274
+ private emitSessionRuntime(entry: SessionRuntimeEntry): void {
275
+ const snapshot = this.snapshotRuntime(entry)
276
+ entry.info = { ...entry.info, ...snapshot }
277
+ for (const listener of this.sessionRuntimeListeners) listener(snapshot)
278
+ }
279
+
280
+ private emitRuntimeActivity(entry: SessionRuntimeEntry, activity: SessionRuntimeActivity | null): void {
281
+ if (entry.info.activity === activity) return
282
+ this.touchRuntime(entry)
283
+ entry.info = { ...entry.info, activity }
284
+ this.emitSessionRuntime(entry)
285
+ }
286
+
287
+ /** Mark a runtime as recently used so eviction picks a genuinely idle one. */
288
+ private touchRuntime(entry: SessionRuntimeEntry): void {
289
+ entry.lastActiveAt = Date.now()
290
+ }
291
+
292
+ /** A runtime owns a Pi child process only while it is starting or running. */
293
+ private isRuntimeLive(entry: SessionRuntimeEntry): boolean {
294
+ const { status } = entry.manager.getStatus()
295
+ return status === 'starting' || status === 'running'
296
+ }
297
+
298
+ /** The open runtime that currently owns a session file, if there is one. */
299
+ private runtimeOwningSessionPath(sessionPath: string): SessionRuntimeEntry | null {
300
+ const ownerId = this.runtimeBySessionPath.get(pathGroupKey(sessionPath))
301
+ if (!ownerId) return null
302
+ return this.sessionRuntimes.get(ownerId) ?? null
303
+ }
304
+
305
+ /**
306
+ * Stop background runtimes until one more live process fits within
307
+ * MAX_LIVE_SESSION_RUNTIMES. Only the process is stopped: the tab stays open
308
+ * and its session file stays on disk, so selecting the tab again starts a
309
+ * fresh Pi on the same session.
310
+ *
311
+ * Never evicted: the incoming runtime, the active one, one that is still
312
+ * starting, and one mid-turn ('working' or 'needs-approval'). When every
313
+ * remaining runtime is protected the budget is deliberately exceeded —
314
+ * killing a turn the user is waiting on is worse than one extra process.
315
+ */
316
+ private enforceLiveRuntimeBudget(incomingRuntimeId: string | null): void {
317
+ const evictable = (entry: SessionRuntimeEntry): boolean =>
318
+ entry.info.runtimeId !== incomingRuntimeId &&
319
+ entry.info.runtimeId !== this.activeRuntimeId &&
320
+ entry.info.activity !== 'working' &&
321
+ entry.info.activity !== 'needs-approval' &&
322
+ // 'running' rules out both a stopped runtime, which frees nothing, and a
323
+ // starting one, whose caller is still awaiting readiness.
324
+ entry.manager.getStatus().status === 'running'
325
+
326
+ let live = [...this.sessionRuntimes.values()].filter(
327
+ (entry) => entry.info.runtimeId !== incomingRuntimeId && this.isRuntimeLive(entry)
328
+ ).length
329
+
330
+ while (live >= MAX_LIVE_SESSION_RUNTIMES) {
331
+ let victim: SessionRuntimeEntry | null = null
332
+ for (const entry of this.sessionRuntimes.values()) {
333
+ if (!evictable(entry)) continue
334
+ // Map iteration follows creation order, so a strict `<` breaks
335
+ // same-millisecond ties in favour of the longest-open tab.
336
+ if (!victim || entry.lastActiveAt < victim.lastActiveAt) victim = entry
337
+ }
338
+ if (!victim) return
339
+ appLog.info(
340
+ 'workspaces',
341
+ `Stopped idle session runtime ${victim.info.runtimeId} to stay within ${MAX_LIVE_SESSION_RUNTIMES} live Pi processes`
342
+ )
343
+ // stopSessionRuntime emits the runtime snapshot, so the renderer marks
344
+ // the tab stopped instead of showing a process that no longer exists.
345
+ this.stopSessionRuntime(victim.info.runtimeId)
346
+ live -= 1
347
+ }
348
+ }
349
+
350
+ private attachSessionRuntime(entry: SessionRuntimeEntry): void {
351
+ const { manager } = entry
352
+ manager.on('status-change', () => {
353
+ entry.info = { ...entry.info, ...manager.getStatus() }
354
+ if (entry.info.status === 'running' && entry.info.activity === 'failed') {
355
+ entry.info = { ...entry.info, activity: null }
356
+ }
357
+ this.emitSessionRuntime(entry)
358
+ })
359
+ // Same refresh for a startup-phase flip, which changes getStatus() output
360
+ // without changing the status value (so no 'status-change' fires).
361
+ manager.on('startup-phase', () => {
362
+ entry.info = { ...entry.info, ...manager.getStatus() }
363
+ this.emitSessionRuntime(entry)
364
+ })
365
+ manager.on('agent_start', () => this.emitRuntimeActivity(entry, 'working'))
366
+ manager.on('agent_end', () => this.emitRuntimeActivity(entry, 'completed'))
367
+ manager.on('extension_ui_request', (event: { method?: string }) => {
368
+ if (event.method === 'select' || event.method === 'confirm' || event.method === 'input' || event.method === 'editor') {
369
+ this.emitRuntimeActivity(entry, 'needs-approval')
370
+ }
371
+ })
372
+ manager.on('exit', () => {
373
+ // PiRpcManager only emits exit for an unexpected process death; deliberate
374
+ // stop() detaches listeners first. Preserve a visible failure marker even
375
+ // when the process died while idle.
376
+ this.emitRuntimeActivity(entry, 'failed')
377
+ })
378
+ }
379
+
380
+ private createSessionRuntime(workspaceId: string, sessionPath: string | null): SessionRuntimeEntry {
381
+ if (sessionPath && this.runtimeOwningSessionPath(sessionPath)) {
382
+ throw new Error(t('errors.session.alreadyAttached'))
383
+ }
384
+ // A new tab is started right after it is created, so make room for its
385
+ // process before the tab exists.
386
+ this.enforceLiveRuntimeBudget(null)
387
+ const runtimeId = `rt-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
388
+ const manager = new PiRpcManager()
389
+ const entry: SessionRuntimeEntry = {
390
+ manager,
391
+ lastActiveAt: Date.now(),
392
+ // A tab opened without a session file gets one written by the agent we
393
+ // spawn for it. A tab opened ON a file adopts a conversation that was
394
+ // already there, which this app never gets to discard.
395
+ appCreated: sessionPath === null,
396
+ info: {
397
+ runtimeId,
398
+ workspaceId,
399
+ sessionPath,
400
+ sessionId: null,
401
+ status: 'stopped',
402
+ pid: null,
403
+ error: null,
404
+ activity: null,
405
+ active: false,
406
+ },
407
+ }
408
+ this.sessionRuntimes.set(runtimeId, entry)
409
+ if (sessionPath) this.runtimeBySessionPath.set(pathGroupKey(sessionPath), runtimeId)
410
+ this.wirePiManager(manager)
411
+ this.attachSessionRuntime(entry)
412
+ this.emitSessionRuntime(entry)
413
+ return entry
414
+ }
415
+
416
+ private setActiveRuntime(workspaceId: string, runtimeId: string | null): void {
417
+ const previous = this.activeRuntimeId
418
+ if (runtimeId) this.activeRuntimeByWorkspace.set(workspaceId, runtimeId)
419
+ else this.activeRuntimeByWorkspace.delete(workspaceId)
420
+ this.activeRuntimeId = this.activeWorkspaceId === workspaceId ? runtimeId : this.activeRuntimeId
421
+ if (previous && previous !== this.activeRuntimeId) {
422
+ const old = this.sessionRuntimes.get(previous)
423
+ if (old) this.emitSessionRuntime(old)
424
+ }
425
+ if (this.activeRuntimeId) {
426
+ const next = this.sessionRuntimes.get(this.activeRuntimeId)
427
+ if (next) {
428
+ this.touchRuntime(next)
429
+ this.emitSessionRuntime(next)
430
+ }
431
+ }
432
+ }
433
+
434
+ getSessionRuntimes(workspaceId?: string): SessionRuntimeInfo[] {
435
+ return [...this.sessionRuntimes.values()]
436
+ .filter((entry) => workspaceId === undefined || entry.info.workspaceId === workspaceId)
437
+ .map((entry) => this.snapshotRuntime(entry))
438
+ }
439
+
440
+ /** Drop inactive header-only sessions so abandoned New Session tabs do not linger. */
441
+ async pruneEmptySessionRuntimes(): Promise<SessionRuntimeCloseResult[]> {
442
+ const candidates = [...this.sessionRuntimes.values()]
443
+ .filter((entry) =>
444
+ entry.info.runtimeId !== this.activeRuntimeId &&
445
+ entry.info.sessionPath &&
446
+ entry.info.activity === null &&
447
+ entry.manager.getStatus().status !== 'starting'
448
+ )
449
+ const closed: SessionRuntimeCloseResult[] = []
450
+ for (const entry of candidates) {
451
+ if (await inspectSessionContent(entry.info.sessionPath!) !== 'empty') continue
452
+ // A candidate may have become active or started work while the bounded
453
+ // file scan was in flight. Re-check before stopping it.
454
+ const current = this.sessionRuntimes.get(entry.info.runtimeId)
455
+ if (
456
+ !current ||
457
+ current.info.runtimeId === this.activeRuntimeId ||
458
+ current.info.activity !== null ||
459
+ current.manager.getStatus().status === 'starting'
460
+ ) continue
461
+ const result = await this.closeSessionRuntime(entry.info.runtimeId)
462
+ if (result) closed.push(result)
463
+ }
464
+ return closed
465
+ }
466
+
467
+ getActiveSessionRuntime(): SessionRuntimeInfo | null {
468
+ if (!this.activeRuntimeId) return null
469
+ const entry = this.sessionRuntimes.get(this.activeRuntimeId)
470
+ return entry ? this.snapshotRuntime(entry) : null
471
+ }
472
+
473
+ getSessionRuntime(runtimeId: string): SessionRuntimeInfo | null {
474
+ const entry = this.sessionRuntimes.get(runtimeId)
475
+ return entry ? this.snapshotRuntime(entry) : null
476
+ }
477
+
478
+ getSessionRuntimeForPath(sessionPath: string): SessionRuntimeInfo | null {
479
+ const runtimeId = this.runtimeBySessionPath.get(pathGroupKey(sessionPath))
480
+ return runtimeId ? this.getSessionRuntime(runtimeId) : null
481
+ }
482
+
483
+ runtimeIdFor(manager: PiRpcManager): string | null {
484
+ for (const [runtimeId, entry] of this.sessionRuntimes) {
485
+ if (entry.manager === manager) return runtimeId
486
+ }
487
+ return this.workspaceIdFor(manager)
488
+ }
489
+ setSessionRuntimeActivity(runtimeId: string, activity: SessionRuntimeActivity | null): void {
490
+ const entry = this.sessionRuntimes.get(runtimeId)
491
+ if (entry) this.emitRuntimeActivity(entry, activity)
492
+ }
493
+
494
+ sessionPathFor(manager: PiRpcManager): string | null {
495
+ for (const entry of this.sessionRuntimes.values()) {
496
+ if (entry.manager === manager) return entry.info.sessionPath
497
+ }
498
+ return null
499
+ }
500
+
501
+ /** Activate a session without waiting for Pi startup. */
502
+ async activateSession(workspaceId: string, sessionPath: string): Promise<SessionRuntimeInfo> {
503
+ const workspace = this.workspaces.find((item) => item.id === workspaceId)
504
+ if (!workspace) throw new Error(t('errors.workspace.notFound', { workspaceId }))
505
+ if (this.activeWorkspaceId !== workspaceId) await this.setActiveWorkspace(workspaceId)
506
+ const key = pathGroupKey(sessionPath)
507
+ let runtimeId = this.runtimeBySessionPath.get(key)
508
+ let entry = runtimeId ? this.sessionRuntimes.get(runtimeId) : undefined
509
+ if (entry && entry.info.workspaceId !== workspaceId) {
510
+ throw new Error(t('errors.session.attachedToDifferentWorkspace'))
511
+ }
512
+ if (!entry) entry = this.createSessionRuntime(workspaceId, sessionPath)
513
+ runtimeId = entry.info.runtimeId
514
+ this.setActiveRuntime(workspaceId, runtimeId)
515
+ this.emitSessionRuntime(entry)
516
+ return this.snapshotRuntime(entry)
517
+ }
518
+
519
+ /** Create an empty session runtime and make it active immediately. */
520
+ async createNewSessionRuntime(workspaceId: string): Promise<SessionRuntimeInfo> {
521
+ const workspace = this.workspaces.find((item) => item.id === workspaceId)
522
+ if (!workspace) throw new Error(t('errors.workspace.notFound', { workspaceId }))
523
+ if (this.activeWorkspaceId !== workspaceId) await this.setActiveWorkspace(workspaceId)
524
+ const entry = this.createSessionRuntime(workspaceId, null)
525
+ this.setActiveRuntime(workspaceId, entry.info.runtimeId)
526
+ return this.snapshotRuntime(entry)
527
+ }
528
+
529
+ async startSessionRuntime(runtimeId: string, options: PiStartOptions = {}): Promise<SessionRuntimeInfo> {
530
+ const entry = this.sessionRuntimes.get(runtimeId)
531
+ if (!entry) throw new Error(t('errors.session.runtimeNotFound', { runtimeId }))
532
+ const workspace = this.workspaces.find((item) => item.id === entry.info.workspaceId)
533
+ if (!workspace) throw new Error(t('errors.workspace.notFound', { workspaceId: entry.info.workspaceId }))
534
+ // A start that binds this runtime to a file it did not write hands it a
535
+ // conversation that predates us, so the file stops being ours to discard.
536
+ // Read the caller's own options, not the merged ones: the merge re-supplies
537
+ // the path this runtime already owns, which is still its own creation.
538
+ const bindsExistingFile =
539
+ options.sessionPath !== undefined ||
540
+ options.forkSessionPath !== undefined ||
541
+ // --continue opens "the most recent session for this cwd", which is some
542
+ // earlier conversation. It only reaches the engine while this runtime has
543
+ // no file of its own; once it has one, the merged --session outranks it
544
+ // (see buildPiArgs) and the runtime re-opens what it created.
545
+ (options.continueSession === true && entry.info.sessionPath === null)
546
+ if (bindsExistingFile) entry.appCreated = false
547
+ const startOptions: PiStartOptions = {
548
+ cwd: workspace.path,
549
+ ...(entry.info.sessionPath && !options.sessionPath && !options.forkSessionPath
550
+ ? { sessionPath: entry.info.sessionPath }
551
+ : {}),
552
+ ...options,
553
+ }
554
+ // Start the engine that owns the session file rather than the configured
555
+ // default: the stores are separate, so only the engine that wrote a session
556
+ // can resume it. An engine the caller named explicitly still wins.
557
+ const ownerEngine = engineForBoundSession(startOptions)
558
+ if (!startOptions.engine && ownerEngine) startOptions.engine = ownerEngine
559
+ // Re-activating an evicted tab spawns a process again, so the budget has to
560
+ // hold here too; an already-live runtime spawns nothing and needs no room.
561
+ if (!this.isRuntimeLive(entry)) this.enforceLiveRuntimeBudget(runtimeId)
562
+ this.touchRuntime(entry)
563
+ await entry.manager.start(startOptions)
564
+ // closeSessionRuntime() can win while startup is waiting for Pi. Do not
565
+ // emit the now-detached entry after the closed marker was broadcast.
566
+ if (this.sessionRuntimes.get(runtimeId) !== entry) {
567
+ return { ...entry.info, ...entry.manager.getStatus(), active: false, closed: true }
568
+ }
569
+ const response = await entry.manager.sendCommand({ type: 'get_state' }).catch(() => null)
570
+ if (this.sessionRuntimes.get(runtimeId) !== entry) {
571
+ return { ...entry.info, ...entry.manager.getStatus(), active: false, closed: true }
572
+ }
573
+ return this.applySessionRuntimeState(entry, response)
574
+ }
575
+
576
+ private applySessionRuntimeState(entry: SessionRuntimeEntry, response: unknown): SessionRuntimeInfo {
577
+ const data = response && typeof response === 'object' && 'data' in response
578
+ ? response.data as { sessionFile?: unknown; sessionId?: unknown }
579
+ : undefined
580
+ const sessionPath = typeof data?.sessionFile === 'string' ? data.sessionFile : entry.info.sessionPath
581
+ if (sessionPath && sessionPath !== entry.info.sessionPath) {
582
+ const owner = this.runtimeOwningSessionPath(sessionPath)
583
+ if (owner && owner.info.runtimeId !== entry.info.runtimeId) {
584
+ // The incumbent keeps the file — the same invariant createSessionRuntime
585
+ // enforces up front with 'Session file is already attached to a live
586
+ // runtime'. Leaving the mapping alone is not enough on its own: the map
587
+ // is only an index, and two Pi processes appending to one session JSONL
588
+ // corrupt it. Stopping the newcomer is what removes the second writer,
589
+ // and it is the safe side to stop because the incumbent may be mid-turn.
590
+ appLog.warn(
591
+ 'workspaces',
592
+ `Session ${sessionPath} already belongs to runtime ${owner.info.runtimeId}; stopped runtime ${entry.info.runtimeId} instead of taking the session over`
593
+ )
594
+ this.stopSessionRuntime(entry.info.runtimeId)
595
+ this.emitRuntimeActivity(entry, 'failed')
596
+ return this.snapshotRuntime(entry)
597
+ }
598
+ if (entry.info.sessionPath) this.runtimeBySessionPath.delete(pathGroupKey(entry.info.sessionPath))
599
+ this.runtimeBySessionPath.set(pathGroupKey(sessionPath), entry.info.runtimeId)
600
+ }
601
+ entry.info = {
602
+ ...entry.info,
603
+ sessionPath: sessionPath ?? null,
604
+ sessionId: typeof data?.sessionId === 'string' ? data.sessionId : entry.info.sessionId,
605
+ ...entry.manager.getStatus(),
606
+ }
607
+ this.emitSessionRuntime(entry)
608
+ return this.snapshotRuntime(entry)
609
+ }
610
+ async restartSessionRuntime(runtimeId: string, options: PiStartOptions = {}): Promise<SessionRuntimeInfo> {
611
+ const entry = this.sessionRuntimes.get(runtimeId)
612
+ if (!entry) throw new Error(t('errors.session.runtimeNotFound', { runtimeId }))
613
+ entry.manager.stop()
614
+ return this.startSessionRuntime(runtimeId, options)
615
+ }
616
+
617
+ async refreshSessionRuntime(runtimeId: string): Promise<SessionRuntimeInfo | null> {
618
+ const entry = this.sessionRuntimes.get(runtimeId)
619
+ if (!entry) return null
620
+ const response = await entry.manager.sendCommand({ type: 'get_state' })
621
+ return this.applySessionRuntimeState(entry, response)
622
+ }
623
+
624
+ stopSessionRuntime(runtimeId: string): void {
625
+ const entry = this.sessionRuntimes.get(runtimeId)
626
+ if (!entry) return
627
+ entry.info = { ...entry.info, activity: null }
628
+ this.emitSessionRuntime(entry)
629
+ entry.manager.stop()
630
+ }
631
+
632
+ /** Close a session tab without deleting a non-empty session from disk. */
633
+ async closeSessionRuntime(runtimeId: string): Promise<SessionRuntimeCloseResult | null> {
634
+ const entry = this.sessionRuntimes.get(runtimeId)
635
+ if (!entry) return null
636
+
637
+ const { workspaceId, sessionPath } = entry.info
638
+ const contentState = sessionPath ? await inspectSessionContent(sessionPath) : 'empty'
639
+ // Unknown is treated as non-empty. A failed/partial metadata read must
640
+ // never turn a real conversation into a destructive delete.
641
+ const empty = contentState === 'empty'
642
+ const wasActive = this.activeRuntimeId === runtimeId
643
+ const replacementCandidates = wasActive
644
+ ? [...this.sessionRuntimes.values()]
645
+ .filter((candidate) => candidate.info.workspaceId === workspaceId && candidate.info.runtimeId !== runtimeId)
646
+ .reverse()
647
+ : []
648
+ const replacement = replacementCandidates.find((candidate) => candidate.info.sessionPath) ?? replacementCandidates[0]
649
+
650
+ entry.info = { ...entry.info, activity: null }
651
+ entry.manager.stop()
652
+ if (sessionPath) this.runtimeBySessionPath.delete(pathGroupKey(sessionPath))
653
+ this.sessionRuntimes.delete(runtimeId)
654
+
655
+ if (this.activeRuntimeByWorkspace.get(workspaceId) === runtimeId) {
656
+ if (replacement) this.activeRuntimeByWorkspace.set(workspaceId, replacement.info.runtimeId)
657
+ else this.activeRuntimeByWorkspace.delete(workspaceId)
658
+ }
659
+ if (wasActive) this.activeRuntimeId = replacement?.info.runtimeId ?? null
660
+
661
+ const closed: SessionRuntimeInfo = {
662
+ ...entry.info,
663
+ ...entry.manager.getStatus(),
664
+ active: false,
665
+ activity: null,
666
+ closed: true,
667
+ }
668
+ for (const listener of this.sessionRuntimeListeners) listener(closed)
669
+ if (replacement && wasActive) this.emitSessionRuntime(replacement)
670
+
671
+ return {
672
+ runtimeId,
673
+ workspaceId,
674
+ sessionPath,
675
+ replacementSessionPath: replacement?.info.sessionPath ?? null,
676
+ empty,
677
+ appCreated: entry.appCreated,
678
+ deleted: false,
679
+ }
680
+ }
681
+
682
+ sendCommandToSessionRuntime(runtimeId: string, command: Record<string, unknown>): Promise<unknown> {
683
+ const entry = this.sessionRuntimes.get(runtimeId)
684
+ if (!entry) return Promise.reject(new Error(t('errors.session.runtimeNotFound', { runtimeId })))
685
+ this.touchRuntime(entry)
686
+ return entry.manager.sendCommand(command)
687
+ }
688
+
689
+ async initialize(): Promise<void> {
690
+ await this.loadWorkspaces()
691
+
692
+ // No workspace is auto-created. On a fresh install (or empty state) the app
693
+ // opens to the home screen with no active workspace; the user opens a folder
694
+ // or resumes a session, each of which creates + activates a workspace on
695
+ // demand. This avoids fabricating a "Home" workspace pointed at the entire
696
+ // home directory — unnecessary setup the user may never use, and a costly
697
+ // recursive file watcher over the home tree (which also trips over
698
+ // permission-protected Windows system paths).
699
+
700
+ // Workspaces loaded from disk don't go through emitActiveWorkspaceChanged,
701
+ // so attach the watcher to the active workspace explicitly here.
702
+ this.updateActiveWatcher()
703
+ }
704
+
705
+ getWorkspaces(): Workspace[] {
706
+ return [...this.workspaces].sort((a, b) => b.lastActiveAt - a.lastActiveAt)
707
+ }
708
+
709
+ getActiveWorkspace(): Workspace | null {
710
+ if (!this.activeWorkspaceId) return null
711
+ return this.workspaces.find((w) => w.id === this.activeWorkspaceId) ?? null
712
+ }
713
+
714
+ getActiveWorkspaceId(): string | null {
715
+ return this.activeWorkspaceId
716
+ }
717
+
718
+ getPiManager(workspaceId: string): PiRpcManager | null {
719
+ const runtimeId = this.activeRuntimeByWorkspace.get(workspaceId)
720
+ if (runtimeId) return this.sessionRuntimes.get(runtimeId)?.manager ?? null
721
+ return this.piManagers.get(workspaceId) ?? null
722
+ }
723
+
724
+ getActivePiManager(): PiRpcManager | null {
725
+ if (!this.activeWorkspaceId) return null
726
+ return this.getPiManager(this.activeWorkspaceId)
727
+ }
728
+
729
+ getPiManagerForSession(workspaceId: string, sessionId: string): PiRpcManager | null {
730
+ for (const entry of this.sessionRuntimes.values()) {
731
+ if (entry.info.workspaceId === workspaceId && entry.info.sessionId === sessionId) return entry.manager
732
+ }
733
+ return null
734
+ }
735
+
736
+ /** Reverse lookup: the workspace id owning a given Pi manager, if any. */
737
+ workspaceIdFor(manager: PiRpcManager): string | null {
738
+ for (const [workspaceId, candidate] of this.piManagers) {
739
+ if (candidate === manager) return workspaceId
740
+ }
741
+ for (const entry of this.sessionRuntimes.values()) {
742
+ if (entry.manager === manager) return entry.info.workspaceId
743
+ }
744
+ return null
745
+ }
746
+
747
+ getFileService(workspaceId: string): FileService | null {
748
+ return this.fileServices.get(workspaceId) ?? null
749
+ }
750
+
751
+ getActiveFileService(): FileService | null {
752
+ if (!this.activeWorkspaceId) return null
753
+ return this.fileServices.get(this.activeWorkspaceId) ?? null
754
+ }
755
+
756
+ async createWorkspace(name: string, path: string): Promise<Workspace> {
757
+ // Check for duplicate path (case-insensitive on Windows, so "Documents"
758
+ // and "documents" resolve to the same workspace instead of duplicating).
759
+ const existing = this.workspaces.find((w) => pathsEqual(w.path, path))
760
+ if (existing) {
761
+ return this.setActiveWorkspace(existing.id)
762
+ }
763
+
764
+ const workspace: Workspace = {
765
+ id: `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
766
+ name,
767
+ path,
768
+ createdAt: Date.now(),
769
+ lastActiveAt: Date.now(),
770
+ color: WORKSPACE_COLORS[this.nextColorIndex % WORKSPACE_COLORS.length],
771
+ kind: 'folder',
772
+ }
773
+
774
+ this.nextColorIndex++
775
+ this.workspaces.push(workspace)
776
+
777
+ // Create Pi manager and file service for this workspace
778
+ const piManager = new PiRpcManager()
779
+ this.piManagers.set(workspace.id, piManager)
780
+ this.wirePiManager(piManager)
781
+ const fileService = new FileService(path)
782
+ this.fileServices.set(workspace.id, fileService)
783
+
784
+ // Auto-set as active if it's the first workspace
785
+ const becameActive = !this.activeWorkspaceId
786
+ if (becameActive) {
787
+ this.activeWorkspaceId = workspace.id
788
+ }
789
+
790
+ await this.saveWorkspaces()
791
+ if (becameActive) this.emitActiveWorkspaceChanged()
792
+ return workspace
793
+ }
794
+
795
+ async setActiveWorkspace(workspaceId: string): Promise<Workspace> {
796
+ const workspace = this.workspaces.find((w) => w.id === workspaceId)
797
+ if (!workspace) throw new Error(t('errors.workspace.notFound', { workspaceId }))
798
+
799
+ const changed = this.activeWorkspaceId !== workspaceId
800
+ workspace.lastActiveAt = Date.now()
801
+ const previousRuntimeId = this.activeRuntimeId
802
+ this.activeWorkspaceId = workspaceId
803
+ this.activeRuntimeId = this.activeRuntimeByWorkspace.get(workspaceId) ?? null
804
+
805
+ await this.saveWorkspaces()
806
+ if (changed) this.emitActiveWorkspaceChanged()
807
+ if (previousRuntimeId && previousRuntimeId !== this.activeRuntimeId) {
808
+ const previous = this.sessionRuntimes.get(previousRuntimeId)
809
+ if (previous) this.emitSessionRuntime(previous)
810
+ }
811
+ if (this.activeRuntimeId) {
812
+ const active = this.sessionRuntimes.get(this.activeRuntimeId)
813
+ if (active) this.emitSessionRuntime(active)
814
+ }
815
+ return workspace
816
+ }
817
+
818
+ async removeWorkspace(workspaceId: string): Promise<WorkspaceRemoveResult> {
819
+ const index = this.workspaces.findIndex((w) => w.id === workspaceId)
820
+ if (index === -1) throw new Error(t('errors.workspace.notFound', { workspaceId }))
821
+ const workspace = this.workspaces[index]
822
+ let worktreeRemoved: boolean | undefined
823
+ let preservedWorktreePath: string | undefined
824
+
825
+ // Stop Pi process and file watcher for this workspace before touching a
826
+ // managed worktree. Git refuses dirty worktree removal, which is exactly
827
+ // the protection we want when a tab is closed with edits still present.
828
+ const piManager = this.piManagers.get(workspaceId)
829
+ if (piManager) {
830
+ await piManager.stopAndWait()
831
+ this.piManagers.delete(workspaceId)
832
+ }
833
+ for (const [runtimeId, entry] of this.sessionRuntimes) {
834
+ if (entry.info.workspaceId !== workspaceId) continue
835
+ if (this.activeRuntimeId === runtimeId) this.activeRuntimeId = null
836
+ await entry.manager.stopAndWait()
837
+ if (entry.info.sessionPath) this.runtimeBySessionPath.delete(pathGroupKey(entry.info.sessionPath))
838
+ this.sessionRuntimes.delete(runtimeId)
839
+ }
840
+ this.activeRuntimeByWorkspace.delete(workspaceId)
841
+ const fileService = this.fileServices.get(workspaceId)
842
+ if (fileService) {
843
+ fileService.stopWatching()
844
+ this.fileServices.delete(workspaceId)
845
+ }
846
+ if (workspace.kind === 'worktree' && workspace.managed !== false && workspace.repoRoot) {
847
+ try {
848
+ await removeGitWorktree(workspace.repoRoot, workspace.path)
849
+ worktreeRemoved = true
850
+ } catch (err) {
851
+ // Keep dirty/missing worktrees on disk instead of forcing deletion.
852
+ preservedWorktreePath = workspace.path
853
+ const detail = err instanceof GitCommandError
854
+ ? describeGitFailure(err.args, err.stdout, err.stderr, tEnglish)
855
+ : err
856
+ appLog.warn('workspaces', 'Preserved managed worktree while closing tab', detail)
857
+ }
858
+ }
859
+
860
+ this.workspaces.splice(index, 1)
861
+
862
+ // If removed workspace was active, switch to first available
863
+ let activeChanged = false
864
+ if (this.activeWorkspaceId === workspaceId) {
865
+ this.activeWorkspaceId = this.workspaces.length > 0 ? this.workspaces[0].id : null
866
+ this.activeRuntimeId = this.activeWorkspaceId
867
+ ? this.activeRuntimeByWorkspace.get(this.activeWorkspaceId) ?? null
868
+ : null
869
+ activeChanged = true
870
+ }
871
+
872
+ await this.saveWorkspaces()
873
+ if (activeChanged) this.emitActiveWorkspaceChanged()
874
+ if (activeChanged && this.activeRuntimeId) {
875
+ const active = this.sessionRuntimes.get(this.activeRuntimeId)
876
+ if (active) this.emitSessionRuntime(active)
877
+ }
878
+ for (const listener of this.workspaceRemovedListeners) {
879
+ listener(workspaceId)
880
+ }
881
+ return { worktreeRemoved, preservedWorktreePath }
882
+ }
883
+
884
+ async renameWorkspace(workspaceId: string, name: string): Promise<void> {
885
+ const workspace = this.workspaces.find((w) => w.id === workspaceId)
886
+ if (!workspace) throw new Error(t('errors.workspace.notFound', { workspaceId }))
887
+
888
+ workspace.name = name
889
+ await this.saveWorkspaces()
890
+ }
891
+
892
+ /**
893
+ * Repoint a workspace at a different folder. Replaces its FileService (which
894
+ * binds the path at construction), stops the workspace's Pi (its cwd is
895
+ * bound at spawn), and re-arms watching if it's the active one. The renderer
896
+ * restarts the active workspace's Pi after this commits.
897
+ */
898
+ async changeWorkspacePath(workspaceId: string, newPath: string): Promise<void> {
899
+ const workspace = this.workspaces.find((w) => w.id === workspaceId)
900
+ if (!workspace) throw new Error(t('errors.workspace.notFound', { workspaceId }))
901
+ if (workspace.kind === 'worktree') {
902
+ throw new Error(t('errors.workspace.managedWorktreeCannotChangeFolder'))
903
+ }
904
+ if (!existsSync(newPath)) throw new Error(t('errors.workspace.folderMissing', { path: newPath }))
905
+
906
+ workspace.path = newPath
907
+ // Pi's working directory is bound at spawn, so every session runtime must
908
+ // stop before the project path changes. The renderer restarts the active
909
+ // runtime after this commits; inactive sessions restart when selected.
910
+ this.piManagers.get(workspaceId)?.stop()
911
+ for (const entry of this.sessionRuntimes.values()) {
912
+ if (entry.info.workspaceId === workspaceId) this.stopSessionRuntime(entry.info.runtimeId)
913
+ }
914
+ const oldFs = this.fileServices.get(workspaceId)
915
+ oldFs?.stopWatching()
916
+ this.fileServices.set(workspaceId, new FileService(newPath))
917
+ await this.saveWorkspaces()
918
+ // Re-arm the watcher if this is the active workspace.
919
+ if (this.activeWorkspaceId === workspaceId) {
920
+ this.watchingWorkspaceId = null
921
+ this.updateActiveWatcher()
922
+ }
923
+ }
924
+
925
+ /** Whether the active workspace's folder currently exists on disk. */
926
+ activeWorkspacePathExists(): boolean {
927
+ const ws = this.getActiveWorkspace()
928
+ return ws ? existsSync(ws.path) : false
929
+ }
930
+
931
+ private async adoptExistingWorktree(
932
+ repoRoot: string,
933
+ entry: { path: string; head: string | null; branch: string | null },
934
+ taskPrompt: string,
935
+ ): Promise<Workspace> {
936
+ const existing = this.workspaces.find((workspace) => pathsEqual(workspace.path, entry.path))
937
+ if (existing) return existing
938
+
939
+ const id = `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
940
+ const branchLabel = entry.branch?.split('/').pop() || basename(entry.path) || 'Existing worktree'
941
+ const workspace: Workspace = {
942
+ id,
943
+ name: branchLabel,
944
+ path: entry.path,
945
+ createdAt: Date.now(),
946
+ lastActiveAt: Date.now(),
947
+ color: WORKSPACE_COLORS[this.nextColorIndex % WORKSPACE_COLORS.length],
948
+ kind: 'worktree',
949
+ repoRoot,
950
+ ...(entry.branch ? { branch: entry.branch } : {}),
951
+ ...(entry.head ? { baseRef: entry.head } : {}),
952
+ managed: false,
953
+ taskPrompt,
954
+ }
955
+ this.nextColorIndex++
956
+ this.workspaces.push(workspace)
957
+ const piManager = new PiRpcManager()
958
+ this.piManagers.set(workspace.id, piManager)
959
+ this.wirePiManager(piManager)
960
+ this.fileServices.set(workspace.id, new FileService(entry.path))
961
+ await this.saveWorkspaces()
962
+ return workspace
963
+ }
964
+
965
+ /** Whether a checkout lives inside the worktree root this app owns. */
966
+ private isManagedWorktreePath(path: string): boolean {
967
+ return isPathWithin(getGuiDataPath(MANAGED_WORKTREES_DIR), path)
968
+ }
969
+
970
+ /**
971
+ * Reuse an existing checkout when the task identifies it safely. Exact task
972
+ * metadata and a GitHub PR head branch are deterministic; a branch named in
973
+ * the task is also accepted, but ambiguous matches are ignored.
974
+ *
975
+ * Only worktrees this app created are ever reusable. The UI promises that the
976
+ * source project stays untouched, so the user's own clone — and any worktree
977
+ * they made themselves — must never be handed to an agent, no matter which
978
+ * branch a task names.
979
+ */
980
+ private async findRelatedWorktree(sourcePath: string, repoRoot: string, taskPrompt: string): Promise<Workspace | null> {
981
+ const sourceResolved = resolve(sourcePath)
982
+ const repoRootResolved = resolve(repoRoot)
983
+ const normalizedTask = taskPrompt.trim().replace(/\s+/g, ' ').toLowerCase()
984
+ if (!normalizedTask) return null
985
+
986
+ const savedMatch = this.workspaces.find((workspace) =>
987
+ workspace.kind === 'worktree' &&
988
+ this.isManagedWorktreePath(workspace.path) &&
989
+ !pathsEqual(resolve(workspace.path), sourceResolved) &&
990
+ workspace.taskPrompt?.trim().replace(/\s+/g, ' ').toLowerCase() === normalizedTask &&
991
+ !!workspace.repoRoot &&
992
+ pathsEqual(workspace.repoRoot, repoRoot) &&
993
+ existsSync(workspace.path)
994
+ )
995
+ if (savedMatch) return savedMatch
996
+
997
+ let pullRequestBranch: string | null = null
998
+ const pullRequestUrl = extractGitHubPullRequestUrl(taskPrompt)
999
+ if (pullRequestUrl) {
1000
+ pullRequestBranch = await resolvePullRequestHeadBranch(sourcePath, pullRequestUrl).catch(() => null)
1001
+ }
1002
+
1003
+ const entries = await listGitWorktrees(sourcePath).catch(() => [])
1004
+ const candidates = entries
1005
+ .filter((entry, index) =>
1006
+ // `git worktree list` prints the main working tree first. That is the
1007
+ // user's primary checkout, so drop it by position and by path.
1008
+ index > 0 &&
1009
+ !pathsEqual(resolve(entry.path), repoRootResolved) &&
1010
+ !entry.bare &&
1011
+ !pathsEqual(resolve(entry.path), sourceResolved) &&
1012
+ // Reuse is limited to the app's own worktree root; a checkout anywhere
1013
+ // else is the user's, not ours to hand over.
1014
+ this.isManagedWorktreePath(entry.path) &&
1015
+ existsSync(entry.path) &&
1016
+ entry.branch
1017
+ )
1018
+ .map((entry) => ({ ...entry, path: resolve(entry.path) }))
1019
+ const matches = candidates.filter((entry) => {
1020
+ const branch = entry.branch!
1021
+ if (pullRequestBranch) return branch === pullRequestBranch
1022
+ // Generated Pi task branches carry the slug of the first task line. A
1023
+ // branch explicitly written in the prompt is also safe when it is not a
1024
+ // generic default branch; never guess from arbitrary short words.
1025
+ const firstLineSlug = taskPrompt.split(/\r?\n/, 1)[0]?.trim().slice(0, 60)
1026
+ const generatedPrefix = firstLineSlug ? `pi/${slugifyWorktreePart(firstLineSlug)}-` : ''
1027
+ return (generatedPrefix && branch.toLowerCase().startsWith(generatedPrefix.toLowerCase())) ||
1028
+ (branch.includes('/') && normalizedTask.includes(branch.toLowerCase()))
1029
+ })
1030
+ if (matches.length !== 1) return null
1031
+
1032
+ const match = matches[0]
1033
+ const existing = this.workspaces.find((workspace) => pathsEqual(workspace.path, match.path))
1034
+ return existing ?? this.adoptExistingWorktree(repoRoot, match, taskPrompt.trim())
1035
+ }
1036
+
1037
+ /**
1038
+ * Create an app-owned Git worktree that becomes an independent tab, unless
1039
+ * the task already points at a related local worktree. New worktrees start
1040
+ * from HEAD; source-tab edits stay in the source tab. The workspace is not
1041
+ * activated here; the renderer performs the normal guarded tab switch.
1042
+ */
1043
+ async createWorktreeWorkspace(options: WorkspaceTabOptions = {}): Promise<Workspace> {
1044
+ const source = options.sourceWorkspaceId
1045
+ ? this.workspaces.find((w) => w.id === options.sourceWorkspaceId)
1046
+ : this.getActiveWorkspace()
1047
+ if (!source) throw new Error(t('errors.workspace.noSourceForNewTab'))
1048
+ if (!existsSync(source.path)) throw new Error(t('errors.workspace.sourceFolderMissing', { path: source.path }))
1049
+
1050
+ const git = await inspectGitRepository(source.path)
1051
+ const taskPrompt = options.taskPrompt?.trim() || ''
1052
+ const pullRequestUrl = extractGitHubPullRequestUrl(taskPrompt)
1053
+ if (taskPrompt) {
1054
+ const related = await this.findRelatedWorktree(source.path, git.repoRoot, taskPrompt)
1055
+ if (related) return related
1056
+ }
1057
+ if (pullRequestUrl) {
1058
+ throw new Error(t('errors.workspace.pullRequestNotCheckedOut'))
1059
+ }
1060
+ const sourceWasDirty = git.status.trim().length > 0
1061
+ const id = `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
1062
+ const label = options.name?.trim() || `${source.name}-tab`
1063
+ const branch = worktreeBranchName(label, id)
1064
+ const targetPath = worktreeTargetPath(getGuiDataPath(MANAGED_WORKTREES_DIR), git.repoRoot, id)
1065
+ await createGitWorktree({ sourceCwd: source.path, targetPath, branch })
1066
+
1067
+ const workspace: Workspace = {
1068
+ id,
1069
+ name: options.name?.trim() || `${source.name} · tab`,
1070
+ path: targetPath,
1071
+ createdAt: Date.now(),
1072
+ lastActiveAt: Date.now(),
1073
+ color: WORKSPACE_COLORS[this.nextColorIndex % WORKSPACE_COLORS.length],
1074
+ kind: 'worktree',
1075
+ repoRoot: git.repoRoot,
1076
+ branch,
1077
+ baseRef: git.head,
1078
+ sourceWasDirty,
1079
+ managed: true,
1080
+ ...(taskPrompt ? { taskPrompt } : {}),
1081
+ }
1082
+ this.nextColorIndex++
1083
+ this.workspaces.push(workspace)
1084
+ const piManager = new PiRpcManager()
1085
+ this.piManagers.set(workspace.id, piManager)
1086
+ this.wirePiManager(piManager)
1087
+ this.fileServices.set(workspace.id, new FileService(targetPath))
1088
+ await this.saveWorkspaces()
1089
+ return workspace
1090
+ }
1091
+
1092
+ async startPiForWorkspace(workspaceId: string, options?: PiStartOptions): Promise<void> {
1093
+ const workspace = this.workspaces.find((w) => w.id === workspaceId)
1094
+ if (!workspace) throw new Error(t('errors.workspace.notFound', { workspaceId }))
1095
+
1096
+ const runtimeId = this.activeRuntimeByWorkspace.get(workspaceId)
1097
+ let runtime = runtimeId ? this.sessionRuntimes.get(runtimeId) : undefined
1098
+ if (!runtime) {
1099
+ runtime = this.createSessionRuntime(workspaceId, options?.sessionPath ?? null)
1100
+ this.activeRuntimeByWorkspace.set(workspaceId, runtime.info.runtimeId)
1101
+ if (this.activeWorkspaceId === workspaceId) this.activeRuntimeId = runtime.info.runtimeId
1102
+ }
1103
+
1104
+ await this.startSessionRuntime(runtime.info.runtimeId, {
1105
+ cwd: workspace.path,
1106
+ ...options,
1107
+ })
1108
+ }
1109
+
1110
+ stopPiForWorkspace(workspaceId: string): void {
1111
+ const runtimeId = this.activeRuntimeByWorkspace.get(workspaceId)
1112
+ const runtime = runtimeId ? this.sessionRuntimes.get(runtimeId) : undefined
1113
+ if (runtime) this.stopSessionRuntime(runtimeId!)
1114
+ else this.piManagers.get(workspaceId)?.stop()
1115
+ }
1116
+
1117
+ stopAll(): void {
1118
+ for (const [, manager] of this.piManagers) manager.stop()
1119
+ for (const [, entry] of this.sessionRuntimes) entry.manager.stop()
1120
+ for (const [, fs] of this.fileServices) fs.stopWatching()
1121
+ this.watchingWorkspaceId = null
1122
+ this.piManagers.clear()
1123
+ this.sessionRuntimes.clear()
1124
+ this.runtimeBySessionPath.clear()
1125
+ this.activeRuntimeByWorkspace.clear()
1126
+ this.activeRuntimeId = null
1127
+ this.fileServices.clear()
1128
+ }
1129
+
1130
+ private async loadWorkspaces(): Promise<void> {
1131
+ // Prefer the live file; fall back to the .bak if the live file is missing
1132
+ // or unparseable (e.g. an external tool corrupted it).
1133
+ const state =
1134
+ (await this.readWorkspaceState(this.configPath)) ??
1135
+ (await this.readWorkspaceState(`${this.configPath}.bak`))
1136
+ if (!state) {
1137
+ this.workspaces = []
1138
+ this.activeWorkspaceId = null
1139
+ return
1140
+ }
1141
+
1142
+ this.workspaces = (state.workspaces ?? []).map((workspace) => ({
1143
+ ...workspace,
1144
+ kind: workspace.kind ?? 'folder',
1145
+ }))
1146
+ this.activeWorkspaceId = state.activeWorkspaceId ?? null
1147
+
1148
+ // Create file services and Pi managers for loaded workspaces
1149
+ for (const ws of this.workspaces) {
1150
+ if (!this.piManagers.has(ws.id)) {
1151
+ const manager = new PiRpcManager()
1152
+ this.piManagers.set(ws.id, manager)
1153
+ this.wirePiManager(manager)
1154
+ }
1155
+ if (!this.fileServices.has(ws.id)) {
1156
+ this.fileServices.set(ws.id, new FileService(ws.path))
1157
+ }
1158
+ }
1159
+ }
1160
+
1161
+ /** Read + parse a workspace-state file, or null if missing/unparseable. */
1162
+ private async readWorkspaceState(path: string): Promise<WorkspaceState | null> {
1163
+ try {
1164
+ if (!existsSync(path)) return null
1165
+ const parsed = JSON.parse(await readFile(path, 'utf-8')) as WorkspaceState
1166
+ if (!parsed || !Array.isArray(parsed.workspaces)) return null
1167
+ return parsed
1168
+ } catch {
1169
+ return null
1170
+ }
1171
+ }
1172
+
1173
+ private async saveWorkspaces(): Promise<void> {
1174
+ try {
1175
+ const dir = dirname(this.configPath)
1176
+ if (!existsSync(dir)) {
1177
+ await mkdir(dir, { recursive: true })
1178
+ }
1179
+
1180
+ const state: WorkspaceState = {
1181
+ workspaces: this.workspaces,
1182
+ activeWorkspaceId: this.activeWorkspaceId,
1183
+ }
1184
+
1185
+ // Keep a backup of the last good file before overwriting.
1186
+ if (existsSync(this.configPath)) {
1187
+ await copyFile(this.configPath, `${this.configPath}.bak`)
1188
+ }
1189
+ // Atomic write: write a temp file then rename over the target so a crash
1190
+ // or partial write can never leave a half-written/corrupt config.
1191
+ const tmpPath = `${this.configPath}.tmp`
1192
+ await writeFile(tmpPath, JSON.stringify(state, null, 2), 'utf-8')
1193
+ await rename(tmpPath, this.configPath)
1194
+ } catch (err) {
1195
+ console.error('Failed to save workspaces:', err)
1196
+ appLog.error('workspaces', 'Failed to save workspaces.json', err)
1197
+ }
1198
+ }
1199
+ }