@zerwiz/ymir 0.1.33 → 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.
- package/CHANGELOG.md +56 -0
- package/apps/README.md +4 -0
- package/apps/hlidskjalf/README.md +68 -0
- package/apps/hlidskjalf/android/app/build.gradle +73 -0
- package/apps/hlidskjalf/android/app/capacitor.build.gradle +19 -0
- package/apps/hlidskjalf/android/app/proguard-rules.pro +21 -0
- package/apps/hlidskjalf/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java +26 -0
- package/apps/hlidskjalf/android/app/src/main/AndroidManifest.xml +41 -0
- package/apps/hlidskjalf/android/app/src/main/java/org/zerwiz/ymir/MainActivity.java +5 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-land-hdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-land-mdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-land-xhdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-land-xxhdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-land-xxxhdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-port-hdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-port-mdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-port-xhdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-port-xxhdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-port-xxxhdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +34 -0
- package/apps/hlidskjalf/android/app/src/main/res/layout/activity_main.xml +12 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/values/ic_launcher_background.xml +4 -0
- package/apps/hlidskjalf/android/app/src/main/res/values/strings.xml +7 -0
- package/apps/hlidskjalf/android/app/src/main/res/values/styles.xml +22 -0
- package/apps/hlidskjalf/android/app/src/main/res/xml/file_paths.xml +5 -0
- package/apps/hlidskjalf/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java +18 -0
- package/apps/hlidskjalf/android/build.gradle +29 -0
- package/apps/hlidskjalf/android/capacitor.settings.gradle +3 -0
- package/apps/hlidskjalf/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/apps/hlidskjalf/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/apps/hlidskjalf/android/gradle.properties +22 -0
- package/apps/hlidskjalf/android/gradlew +248 -0
- package/apps/hlidskjalf/android/gradlew.bat +92 -0
- package/apps/hlidskjalf/android/settings.gradle +5 -0
- package/apps/hlidskjalf/android/variables.gradle +16 -0
- package/apps/hlidskjalf/capacitor.config.ts +26 -0
- package/apps/hlidskjalf/dist/apple-touch-icon.png +0 -0
- package/apps/hlidskjalf/dist/assets/index-BWmNIyaG.css +1 -0
- package/apps/hlidskjalf/dist/assets/index-k7QEfNto.js +88 -0
- package/apps/hlidskjalf/dist/assets/index-k7QEfNto.js.map +1 -0
- package/apps/hlidskjalf/dist/icon.svg +7 -0
- package/apps/hlidskjalf/dist/index.html +64 -0
- package/apps/hlidskjalf/dist/manifest.webmanifest +31 -0
- package/apps/hlidskjalf/dist/og.jpg +0 -0
- package/apps/hlidskjalf/dist/og.png +0 -0
- package/apps/hlidskjalf/dist/og.svg +64 -0
- package/apps/hlidskjalf/dist/sw.js +39 -0
- package/apps/hlidskjalf/dist/ymir-icon.png +0 -0
- package/apps/hlidskjalf/dist/ymir-mark.svg +62 -0
- package/apps/hlidskjalf/electron/icon.png +0 -0
- package/apps/hlidskjalf/electron/main.cjs +236 -0
- package/apps/hlidskjalf/electron/preload.cjs +8 -0
- package/apps/hlidskjalf/electron/smidja-icon.png +0 -0
- package/apps/hlidskjalf/electron/ymir-hlidskjalf.desktop.in +12 -0
- package/apps/hlidskjalf/electron/ymir-smidja.desktop.in +12 -0
- package/apps/hlidskjalf/index.html +63 -0
- package/apps/hlidskjalf/package-lock.json +5398 -0
- package/apps/hlidskjalf/package.json +55 -0
- package/apps/hlidskjalf/public/apple-touch-icon.png +0 -0
- package/apps/hlidskjalf/public/icon.svg +7 -0
- package/apps/hlidskjalf/public/manifest.webmanifest +31 -0
- package/apps/hlidskjalf/public/og.jpg +0 -0
- package/apps/hlidskjalf/public/og.png +0 -0
- package/apps/hlidskjalf/public/og.svg +64 -0
- package/apps/hlidskjalf/public/sw.js +39 -0
- package/apps/hlidskjalf/public/ymir-icon.png +0 -0
- package/apps/hlidskjalf/public/ymir-mark.svg +62 -0
- package/apps/hlidskjalf/server/accounts.ts +159 -0
- package/apps/hlidskjalf/server/index.ts +2045 -0
- package/apps/hlidskjalf/src/app/App.tsx +116 -0
- package/apps/hlidskjalf/src/app/BottomStream.tsx +128 -0
- package/apps/hlidskjalf/src/app/MobileNav.tsx +87 -0
- package/apps/hlidskjalf/src/app/Rail.tsx +106 -0
- package/apps/hlidskjalf/src/app/Shell.tsx +135 -0
- package/apps/hlidskjalf/src/app/Topbar.tsx +130 -0
- package/apps/hlidskjalf/src/components/AccentPicker.tsx +207 -0
- package/apps/hlidskjalf/src/components/AccountMenu.tsx +115 -0
- package/apps/hlidskjalf/src/components/AgentCard.tsx +75 -0
- package/apps/hlidskjalf/src/components/EmberBackground.tsx +36 -0
- package/apps/hlidskjalf/src/components/Emblem.tsx +48 -0
- package/apps/hlidskjalf/src/components/Halls.tsx +155 -0
- package/apps/hlidskjalf/src/components/LoginModal.tsx +329 -0
- package/apps/hlidskjalf/src/components/Markdown.tsx +10 -0
- package/apps/hlidskjalf/src/components/MetricTile.tsx +40 -0
- package/apps/hlidskjalf/src/components/ModelPicker.tsx +100 -0
- package/apps/hlidskjalf/src/components/Overlay.tsx +134 -0
- package/apps/hlidskjalf/src/components/PRCard.tsx +154 -0
- package/apps/hlidskjalf/src/components/RecallPanel.tsx +116 -0
- package/apps/hlidskjalf/src/components/RuneTag.tsx +23 -0
- package/apps/hlidskjalf/src/components/Status.tsx +35 -0
- package/apps/hlidskjalf/src/components/TaskChip.tsx +23 -0
- package/apps/hlidskjalf/src/components/TraceRow.tsx +28 -0
- package/apps/hlidskjalf/src/data/lore.ts +127 -0
- package/apps/hlidskjalf/src/data/metadata.ts +147 -0
- package/apps/hlidskjalf/src/data/mythology.ts +95 -0
- package/apps/hlidskjalf/src/data/realms.ts +167 -0
- package/apps/hlidskjalf/src/gates/Cron.tsx +77 -0
- package/apps/hlidskjalf/src/gates/Decisions.tsx +67 -0
- package/apps/hlidskjalf/src/gates/Files.tsx +138 -0
- package/apps/hlidskjalf/src/gates/Fleet.tsx +184 -0
- package/apps/hlidskjalf/src/gates/Forge.tsx +486 -0
- package/apps/hlidskjalf/src/gates/OmniChat.tsx +217 -0
- package/apps/hlidskjalf/src/gates/Processes.tsx +162 -0
- package/apps/hlidskjalf/src/gates/Profile.tsx +225 -0
- package/apps/hlidskjalf/src/gates/Reviews.tsx +33 -0
- package/apps/hlidskjalf/src/gates/Runes.tsx +116 -0
- package/apps/hlidskjalf/src/gates/Runtime.tsx +106 -0
- package/apps/hlidskjalf/src/gates/Sessions.tsx +109 -0
- package/apps/hlidskjalf/src/gates/Stats.tsx +268 -0
- package/apps/hlidskjalf/src/gates/Tasks.tsx +173 -0
- package/apps/hlidskjalf/src/gates/Trace.tsx +155 -0
- package/apps/hlidskjalf/src/gates/Well.tsx +154 -0
- package/apps/hlidskjalf/src/gates/Worktrees.tsx +48 -0
- package/apps/hlidskjalf/src/hooks/useTenantDef.ts +13 -0
- package/apps/hlidskjalf/src/main.tsx +31 -0
- package/apps/hlidskjalf/src/services/api.ts +404 -0
- package/apps/hlidskjalf/src/services/auth.ts +107 -0
- package/apps/hlidskjalf/src/services/stream.ts +35 -0
- package/apps/hlidskjalf/src/state/store.ts +648 -0
- package/apps/hlidskjalf/src/state/ui.ts +62 -0
- package/apps/hlidskjalf/src/styles/auth.css +485 -0
- package/apps/hlidskjalf/src/styles/components.css +1259 -0
- package/apps/hlidskjalf/src/styles/forge.css +115 -0
- package/apps/hlidskjalf/src/styles/global.css +167 -0
- package/apps/hlidskjalf/src/styles/menus.css +175 -0
- package/apps/hlidskjalf/src/styles/mobile.css +298 -0
- package/apps/hlidskjalf/src/styles/overlays.css +205 -0
- package/apps/hlidskjalf/src/styles/shell.css +641 -0
- package/apps/hlidskjalf/src/types.ts +251 -0
- package/apps/hlidskjalf/src/vite-env.d.ts +9 -0
- package/apps/hlidskjalf/static/landing.md +6 -0
- package/apps/hlidskjalf/tsconfig.json +25 -0
- package/apps/hlidskjalf/vite.config.ts +49 -0
- package/apps/hlidskjalf-mobile/App.tsx +93 -0
- package/apps/hlidskjalf-mobile/app.json +13 -0
- package/apps/hlidskjalf-mobile/index.ts +4 -0
- package/apps/hlidskjalf-mobile/package.json +35 -0
- package/apps/hlidskjalf-mobile/public/icon.svg +7 -0
- package/apps/hlidskjalf-mobile/tsconfig.json +4 -0
- package/apps/odrerir/.astro/content.d.ts +159 -0
- package/apps/odrerir/.astro/dev.json +24 -0
- package/apps/odrerir/.astro/settings.json +5 -0
- package/apps/odrerir/.astro/types.d.ts +2 -0
- package/apps/odrerir/astro.config.mjs +79 -0
- package/apps/odrerir/dist/apple-touch-icon.png +0 -0
- package/apps/odrerir/dist/favicon-192.png +0 -0
- package/apps/odrerir/dist/favicon-32.png +0 -0
- package/apps/odrerir/dist/favicon-512.png +0 -0
- package/apps/odrerir/dist/favicon.svg +53 -0
- package/apps/odrerir/dist/icon.svg +7 -0
- package/apps/odrerir/dist/index.html +837 -0
- package/apps/odrerir/dist/og-square.png +0 -0
- package/apps/odrerir/dist/og-story.png +0 -0
- package/apps/odrerir/dist/og.png +0 -0
- package/apps/odrerir/dist/safari-pinned-tab.svg +7 -0
- package/apps/odrerir/dist/site.webmanifest +16 -0
- package/apps/odrerir/dist/styles/cloth.css +112 -0
- package/apps/odrerir/dist/styles/livehall.css +278 -0
- package/apps/odrerir/electron/icon.png +0 -0
- package/apps/odrerir/electron/main.cjs +183 -0
- package/apps/odrerir/electron/ymir-odrerir.desktop.in +12 -0
- package/apps/odrerir/package-lock.json +4195 -0
- package/apps/odrerir/package.json +30 -0
- package/apps/odrerir/public/apple-touch-icon.png +0 -0
- package/apps/odrerir/public/favicon-192.png +0 -0
- package/apps/odrerir/public/favicon-32.png +0 -0
- package/apps/odrerir/public/favicon-512.png +0 -0
- package/apps/odrerir/public/favicon.svg +53 -0
- package/apps/odrerir/public/icon.svg +7 -0
- package/apps/odrerir/public/og-square.png +0 -0
- package/apps/odrerir/public/og-story.png +0 -0
- package/apps/odrerir/public/og.png +0 -0
- package/apps/odrerir/public/safari-pinned-tab.svg +7 -0
- package/apps/odrerir/public/site.webmanifest +16 -0
- package/apps/odrerir/scripts/extract-cloth.mjs +101 -0
- package/apps/odrerir/src/index.html +837 -0
- package/apps/odrerir/src/styles/cloth.css +112 -0
- package/apps/odrerir/src/styles/livehall.css +278 -0
- package/apps/sessrumnir/.github/ISSUE_TEMPLATE/bug_report.yml +99 -0
- package/apps/sessrumnir/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/apps/sessrumnir/.github/ISSUE_TEMPLATE/feature_request.yml +46 -0
- package/apps/sessrumnir/.github/PULL_REQUEST_TEMPLATE.md +56 -0
- package/apps/sessrumnir/.github/SUPPORT.md +18 -0
- package/apps/sessrumnir/.github/workflows/build.yml +133 -0
- package/apps/sessrumnir/AGENTS.md +540 -0
- package/apps/sessrumnir/CLA.md +63 -0
- package/apps/sessrumnir/CONTRIBUTING.md +233 -0
- package/apps/sessrumnir/LICENSE +200 -0
- package/apps/sessrumnir/NOTICE +33 -0
- package/apps/sessrumnir/README.md +329 -0
- package/apps/sessrumnir/bin/sessrumnir-web.sh +117 -0
- package/apps/sessrumnir/bin/sessrumnir.js +169 -0
- package/apps/sessrumnir/docs/CNAME +1 -0
- package/apps/sessrumnir/docs/favicon.ico +0 -0
- package/apps/sessrumnir/docs/icon-512.png +0 -0
- package/apps/sessrumnir/docs/index.html +441 -0
- package/apps/sessrumnir/docs/robots.txt +4 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_181929.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182005.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182020.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182039.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182117.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182225.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182248.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182322.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182340.png +0 -0
- package/apps/sessrumnir/docs/sitemap.xml +9 -0
- package/apps/sessrumnir/electron.vite.config.ts +49 -0
- package/apps/sessrumnir/eslint.config.mjs +75 -0
- package/apps/sessrumnir/i18next.config.ts +33 -0
- package/apps/sessrumnir/install.sh +144 -0
- package/apps/sessrumnir/out/main/index.js +10881 -0
- package/apps/sessrumnir/out/preload/index.js +722 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-400-normal-C8QS47vb.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-400-normal-D3EsxgFc.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-500-normal-B7dJQtg-.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-500-normal-BLlg2W5x.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-600-normal-BcIsDXzC.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-600-normal-CBymlcNF.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-400-normal-Bgrpe4p1.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-400-normal-BlcaxZtM.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-500-normal-CdQuyvtc.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-500-normal-pZw22qtS.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-600-normal-Dr-wLyOm.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-600-normal-dLR2-a_v.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-400-normal-BGH8Vunh.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-400-normal-C3_-2Ua-.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-500-normal-Dj3SQ6fR.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-500-normal-EBdSCOD3.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-600-normal-B254Xisw.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-600-normal-DUW-TI76.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-400-normal-DuQ88yz3.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-400-normal-DuXFa1Dr.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-500-normal-AH9qog1s.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-500-normal-DAuUCO41.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-600-normal-CD-dlDpD.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-600-normal-D3T6BBXG.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-400-normal-C-RiYxEf.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-400-normal-DmUuA7Y2.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-500-normal-DsPuwQHi.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-500-normal-tGBW_mI7.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-600-normal-Dar_cCT6.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-600-normal-GIs8CF2x.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-300-normal-Ba-HN6uq.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-300-normal-ByB2hQUF.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-300-normal-B8O3uHbr.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-300-normal-BbtBtHoB.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-300-normal-BPU54Tw3.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-300-normal-BRS-C4KA.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-300-normal-BEtQ2wm2.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-300-normal-CQbCaAQ4.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-300-normal-B6fSpYYV.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-300-normal-CpdbWO_2.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/index-BxqsRq2x.css +4858 -0
- package/apps/sessrumnir/out/renderer/assets/index-TaD69rFk.js +99435 -0
- package/apps/sessrumnir/out/renderer/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/newsreader-latin-ext-wght-italic-DFS986qL.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/newsreader-latin-ext-wght-normal-C-3rgBeH.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/newsreader-latin-wght-italic-Bxi8ein9.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/newsreader-latin-wght-normal-CCVVNp6i.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/newsreader-vietnamese-wght-italic-xAqemagA.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/newsreader-vietnamese-wght-normal-Czsa-EzN.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/openmoji-color-9qo_2DLG.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/pi-logo-B6SVnhuS.svg +7 -0
- package/apps/sessrumnir/out/renderer/index.html +19 -0
- package/apps/sessrumnir/out/renderer/theme-boot.js +30 -0
- package/apps/sessrumnir/package-lock.json +10636 -0
- package/apps/sessrumnir/package.json +183 -0
- package/apps/sessrumnir/public/apple-touch-icon.png +0 -0
- package/apps/sessrumnir/public/icon.svg +7 -0
- package/apps/sessrumnir/resources/icons/generate_icons.py +38 -0
- package/apps/sessrumnir/resources/icons/icon-128.png +0 -0
- package/apps/sessrumnir/resources/icons/icon-16.png +0 -0
- package/apps/sessrumnir/resources/icons/icon-256.png +0 -0
- package/apps/sessrumnir/resources/icons/icon-32.png +0 -0
- package/apps/sessrumnir/resources/icons/icon-48.png +0 -0
- package/apps/sessrumnir/resources/icons/icon-512.png +0 -0
- package/apps/sessrumnir/resources/icons/icon-64.png +0 -0
- package/apps/sessrumnir/resources/icons/icon.icns +0 -0
- package/apps/sessrumnir/resources/icons/icon.ico +0 -0
- package/apps/sessrumnir/resources/icons/icon.png +0 -0
- package/apps/sessrumnir/resources/icons/icon.svg +7 -0
- package/apps/sessrumnir/resources/linux/after-install.sh +23 -0
- package/apps/sessrumnir/resources/linux/after-remove.sh +18 -0
- package/apps/sessrumnir/resources/locales/en/translation.json +1735 -0
- package/apps/sessrumnir/resources/permission-prompt-text.test.ts +111 -0
- package/apps/sessrumnir/resources/permission-prompt-text.ts +84 -0
- package/apps/sessrumnir/resources/permission-rules.test.ts +351 -0
- package/apps/sessrumnir/resources/permission-rules.ts +293 -0
- package/apps/sessrumnir/resources/pi-desktop-permissions.ts +62 -0
- package/apps/sessrumnir/resources/ymir-sessrumnir.desktop.in +12 -0
- package/apps/sessrumnir/scripts/check-semantic-colors.mjs +57 -0
- package/apps/sessrumnir/src/main/activity-stats.test.ts +240 -0
- package/apps/sessrumnir/src/main/activity-stats.ts +596 -0
- package/apps/sessrumnir/src/main/agent-detection.test.ts +25 -0
- package/apps/sessrumnir/src/main/agent-detection.ts +114 -0
- package/apps/sessrumnir/src/main/app-data-paths.test.ts +90 -0
- package/apps/sessrumnir/src/main/app-data-paths.ts +95 -0
- package/apps/sessrumnir/src/main/app-log.test.ts +114 -0
- package/apps/sessrumnir/src/main/app-log.ts +210 -0
- package/apps/sessrumnir/src/main/archived-sessions.test.ts +50 -0
- package/apps/sessrumnir/src/main/archived-sessions.ts +95 -0
- package/apps/sessrumnir/src/main/attachment-reader.test.ts +56 -0
- package/apps/sessrumnir/src/main/attachment-reader.ts +52 -0
- package/apps/sessrumnir/src/main/auto-tag.test.ts +38 -0
- package/apps/sessrumnir/src/main/auto-tag.ts +75 -0
- package/apps/sessrumnir/src/main/autostart-linux.test.ts +56 -0
- package/apps/sessrumnir/src/main/autostart-linux.ts +58 -0
- package/apps/sessrumnir/src/main/cmd-escape.test.ts +424 -0
- package/apps/sessrumnir/src/main/cmd-escape.ts +186 -0
- package/apps/sessrumnir/src/main/council-manager.test.ts +167 -0
- package/apps/sessrumnir/src/main/council-manager.ts +265 -0
- package/apps/sessrumnir/src/main/diagnostics-report.test.ts +140 -0
- package/apps/sessrumnir/src/main/diagnostics-report.ts +91 -0
- package/apps/sessrumnir/src/main/diagnostics.ts +111 -0
- package/apps/sessrumnir/src/main/editor-guard.test.ts +59 -0
- package/apps/sessrumnir/src/main/editor-guard.ts +50 -0
- package/apps/sessrumnir/src/main/extension-ui-ipc.test.ts +299 -0
- package/apps/sessrumnir/src/main/extension-ui-ipc.ts +117 -0
- package/apps/sessrumnir/src/main/file-service.test.ts +279 -0
- package/apps/sessrumnir/src/main/file-service.ts +652 -0
- package/apps/sessrumnir/src/main/fs-errors.ts +42 -0
- package/apps/sessrumnir/src/main/get-messages-trim.test.ts +77 -0
- package/apps/sessrumnir/src/main/get-messages-trim.ts +97 -0
- package/apps/sessrumnir/src/main/git-conveyor.test.ts +305 -0
- package/apps/sessrumnir/src/main/git-conveyor.ts +338 -0
- package/apps/sessrumnir/src/main/git-worktree.test.ts +101 -0
- package/apps/sessrumnir/src/main/git-worktree.ts +201 -0
- package/apps/sessrumnir/src/main/i18n.ts +31 -0
- package/apps/sessrumnir/src/main/index.ts +619 -0
- package/apps/sessrumnir/src/main/ipc/active-engine.ts +15 -0
- package/apps/sessrumnir/src/main/ipc/context.ts +58 -0
- package/apps/sessrumnir/src/main/ipc/council-handlers.ts +126 -0
- package/apps/sessrumnir/src/main/ipc/diagnostics-handlers.ts +8 -0
- package/apps/sessrumnir/src/main/ipc/file-handlers.ts +96 -0
- package/apps/sessrumnir/src/main/ipc/git-conveyor-handlers.ts +55 -0
- package/apps/sessrumnir/src/main/ipc/model-handlers.ts +33 -0
- package/apps/sessrumnir/src/main/ipc/models-config-handlers.ts +80 -0
- package/apps/sessrumnir/src/main/ipc/notes-handlers.ts +65 -0
- package/apps/sessrumnir/src/main/ipc/package-handlers.ts +330 -0
- package/apps/sessrumnir/src/main/ipc/permission-rules-handlers.ts +223 -0
- package/apps/sessrumnir/src/main/ipc/pi-handlers.ts +136 -0
- package/apps/sessrumnir/src/main/ipc/pi-start-options.ts +150 -0
- package/apps/sessrumnir/src/main/ipc/run-pi-cli.ts +53 -0
- package/apps/sessrumnir/src/main/ipc/session-handlers.ts +491 -0
- package/apps/sessrumnir/src/main/ipc/settings.ts +120 -0
- package/apps/sessrumnir/src/main/ipc/skills-mcp-handlers.ts +192 -0
- package/apps/sessrumnir/src/main/ipc/system-handlers.ts +131 -0
- package/apps/sessrumnir/src/main/ipc/tag-handlers.ts +69 -0
- package/apps/sessrumnir/src/main/ipc/terminal-handlers.ts +41 -0
- package/apps/sessrumnir/src/main/ipc/theme-handlers.ts +131 -0
- package/apps/sessrumnir/src/main/ipc/update-handlers.ts +61 -0
- package/apps/sessrumnir/src/main/ipc/validation.ts +45 -0
- package/apps/sessrumnir/src/main/ipc/workflow-handlers.ts +108 -0
- package/apps/sessrumnir/src/main/ipc/workspace-activity-wiring.ts +179 -0
- package/apps/sessrumnir/src/main/ipc/workspace-handlers.ts +133 -0
- package/apps/sessrumnir/src/main/ipc-handlers.ts +98 -0
- package/apps/sessrumnir/src/main/map-concurrent.test.ts +80 -0
- package/apps/sessrumnir/src/main/map-concurrent.ts +30 -0
- package/apps/sessrumnir/src/main/models-file.test.ts +114 -0
- package/apps/sessrumnir/src/main/models-file.ts +89 -0
- package/apps/sessrumnir/src/main/notes-manager.test.ts +64 -0
- package/apps/sessrumnir/src/main/notes-manager.ts +183 -0
- package/apps/sessrumnir/src/main/notify-decision.test.ts +39 -0
- package/apps/sessrumnir/src/main/notify-decision.ts +25 -0
- package/apps/sessrumnir/src/main/omp-fork-points.test.ts +119 -0
- package/apps/sessrumnir/src/main/omp-fork-points.ts +105 -0
- package/apps/sessrumnir/src/main/omp-plugin-list.test.ts +94 -0
- package/apps/sessrumnir/src/main/omp-plugin-list.ts +103 -0
- package/apps/sessrumnir/src/main/package-catalog.test.ts +157 -0
- package/apps/sessrumnir/src/main/package-catalog.ts +166 -0
- package/apps/sessrumnir/src/main/package-updates.test.ts +149 -0
- package/apps/sessrumnir/src/main/package-updates.ts +185 -0
- package/apps/sessrumnir/src/main/path-authorization.test.ts +64 -0
- package/apps/sessrumnir/src/main/path-authorization.ts +31 -0
- package/apps/sessrumnir/src/main/pi-binary-resolution.test.ts +582 -0
- package/apps/sessrumnir/src/main/pi-binary-resolution.ts +649 -0
- package/apps/sessrumnir/src/main/pi-event-router.test.ts +388 -0
- package/apps/sessrumnir/src/main/pi-event-router.ts +290 -0
- package/apps/sessrumnir/src/main/pi-paths.test.ts +134 -0
- package/apps/sessrumnir/src/main/pi-paths.ts +74 -0
- package/apps/sessrumnir/src/main/pi-rpc-manager.test.ts +467 -0
- package/apps/sessrumnir/src/main/pi-rpc-manager.ts +1319 -0
- package/apps/sessrumnir/src/main/process-tree.test.ts +94 -0
- package/apps/sessrumnir/src/main/renderer-origin.test.ts +33 -0
- package/apps/sessrumnir/src/main/renderer-origin.ts +35 -0
- package/apps/sessrumnir/src/main/session-lineage-reader.test.ts +317 -0
- package/apps/sessrumnir/src/main/session-lineage-reader.ts +111 -0
- package/apps/sessrumnir/src/main/session-metadata.test.ts +508 -0
- package/apps/sessrumnir/src/main/session-metadata.ts +526 -0
- package/apps/sessrumnir/src/main/session-name.test.ts +71 -0
- package/apps/sessrumnir/src/main/session-name.ts +69 -0
- package/apps/sessrumnir/src/main/session-paths.test.ts +110 -0
- package/apps/sessrumnir/src/main/session-paths.ts +88 -0
- package/apps/sessrumnir/src/main/session-tags.test.ts +61 -0
- package/apps/sessrumnir/src/main/session-tags.ts +253 -0
- package/apps/sessrumnir/src/main/session-trash.test.ts +66 -0
- package/apps/sessrumnir/src/main/session-trash.ts +65 -0
- package/apps/sessrumnir/src/main/settings-logic.ts +66 -0
- package/apps/sessrumnir/src/main/skills-discovery.test.ts +149 -0
- package/apps/sessrumnir/src/main/skills-discovery.ts +179 -0
- package/apps/sessrumnir/src/main/startup-launch.ts +63 -0
- package/apps/sessrumnir/src/main/terminal-service.ts +96 -0
- package/apps/sessrumnir/src/main/theme-store.test.ts +440 -0
- package/apps/sessrumnir/src/main/theme-store.ts +495 -0
- package/apps/sessrumnir/src/main/tray-decision.test.ts +46 -0
- package/apps/sessrumnir/src/main/tray-decision.ts +49 -0
- package/apps/sessrumnir/src/main/tray-manager.ts +220 -0
- package/apps/sessrumnir/src/main/tsconfig.json +20 -0
- package/apps/sessrumnir/src/main/window-background.test.ts +29 -0
- package/apps/sessrumnir/src/main/window-background.ts +16 -0
- package/apps/sessrumnir/src/main/workflow-discovery.test.ts +252 -0
- package/apps/sessrumnir/src/main/workflow-monitor.test.ts +371 -0
- package/apps/sessrumnir/src/main/workflow-monitor.ts +1020 -0
- package/apps/sessrumnir/src/main/workspace-activity.test.ts +227 -0
- package/apps/sessrumnir/src/main/workspace-activity.ts +215 -0
- package/apps/sessrumnir/src/main/workspace-manager.test.ts +680 -0
- package/apps/sessrumnir/src/main/workspace-manager.ts +1199 -0
- package/apps/sessrumnir/src/main/workspace-trust.test.ts +78 -0
- package/apps/sessrumnir/src/main/workspace-trust.ts +84 -0
- package/apps/sessrumnir/src/preload/index.ts +358 -0
- package/apps/sessrumnir/src/preload/tsconfig.json +20 -0
- package/apps/sessrumnir/src/renderer/index.html +18 -0
- package/apps/sessrumnir/src/renderer/public/theme-boot.js +30 -0
- package/apps/sessrumnir/src/renderer/src/app.tsx +194 -0
- package/apps/sessrumnir/src/renderer/src/assets/openmoji-color.woff2 +0 -0
- package/apps/sessrumnir/src/renderer/src/assets/pi-logo.svg +7 -0
- package/apps/sessrumnir/src/renderer/src/assets/ymir-mark.svg +55 -0
- package/apps/sessrumnir/src/renderer/src/assets.d.ts +7 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-code-highlight.ts +239 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-file-link.ts +140 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-input.tsx +729 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-panel-widths.test.ts +175 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-panel-widths.ts +100 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-panel.tsx +523 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-project-picker.tsx +193 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-search.tsx +251 -0
- package/apps/sessrumnir/src/renderer/src/components/code-editor-highlight.ts +85 -0
- package/apps/sessrumnir/src/renderer/src/components/code-editor-language.test.ts +19 -0
- package/apps/sessrumnir/src/renderer/src/components/code-editor-language.ts +88 -0
- package/apps/sessrumnir/src/renderer/src/components/code-editor.tsx +128 -0
- package/apps/sessrumnir/src/renderer/src/components/command-palette.tsx +315 -0
- package/apps/sessrumnir/src/renderer/src/components/command-results.tsx +80 -0
- package/apps/sessrumnir/src/renderer/src/components/composer-permission-menu.tsx +91 -0
- package/apps/sessrumnir/src/renderer/src/components/context-menu.tsx +457 -0
- package/apps/sessrumnir/src/renderer/src/components/copy-button.tsx +36 -0
- package/apps/sessrumnir/src/renderer/src/components/council-panels.tsx +193 -0
- package/apps/sessrumnir/src/renderer/src/components/custom-models-editor.tsx +330 -0
- package/apps/sessrumnir/src/renderer/src/components/diagnostics-panel.tsx +380 -0
- package/apps/sessrumnir/src/renderer/src/components/diff-viewer.tsx +318 -0
- package/apps/sessrumnir/src/renderer/src/components/ember-background.tsx +113 -0
- package/apps/sessrumnir/src/renderer/src/components/error-boundary.tsx +38 -0
- package/apps/sessrumnir/src/renderer/src/components/extension-ui-dialog-helpers.test.ts +43 -0
- package/apps/sessrumnir/src/renderer/src/components/extension-ui-dialog-helpers.ts +51 -0
- package/apps/sessrumnir/src/renderer/src/components/extension-ui-dialog.tsx +486 -0
- package/apps/sessrumnir/src/renderer/src/components/file-tree.tsx +784 -0
- package/apps/sessrumnir/src/renderer/src/components/git-conveyor-actions.tsx +246 -0
- package/apps/sessrumnir/src/renderer/src/components/home-screen.tsx +396 -0
- package/apps/sessrumnir/src/renderer/src/components/image-viewer.tsx +100 -0
- package/apps/sessrumnir/src/renderer/src/components/line-numbered-code.tsx +58 -0
- package/apps/sessrumnir/src/renderer/src/components/markdown-renderer.tsx +237 -0
- package/apps/sessrumnir/src/renderer/src/components/message-bubble.tsx +1006 -0
- package/apps/sessrumnir/src/renderer/src/components/mission-control.tsx +341 -0
- package/apps/sessrumnir/src/renderer/src/components/model-selector.tsx +219 -0
- package/apps/sessrumnir/src/renderer/src/components/note-picker.tsx +155 -0
- package/apps/sessrumnir/src/renderer/src/components/notes-panel.tsx +437 -0
- package/apps/sessrumnir/src/renderer/src/components/package-browser.tsx +604 -0
- package/apps/sessrumnir/src/renderer/src/components/permission-mode.test.ts +22 -0
- package/apps/sessrumnir/src/renderer/src/components/permission-mode.ts +49 -0
- package/apps/sessrumnir/src/renderer/src/components/permission-rules-editor-helpers.test.ts +44 -0
- package/apps/sessrumnir/src/renderer/src/components/permission-rules-editor-helpers.ts +35 -0
- package/apps/sessrumnir/src/renderer/src/components/permission-rules-editor.tsx +226 -0
- package/apps/sessrumnir/src/renderer/src/components/permission-selector.tsx +104 -0
- package/apps/sessrumnir/src/renderer/src/components/resize-handle.tsx +51 -0
- package/apps/sessrumnir/src/renderer/src/components/review-rail.tsx +194 -0
- package/apps/sessrumnir/src/renderer/src/components/session-menu-position.test.ts +22 -0
- package/apps/sessrumnir/src/renderer/src/components/session-menu-position.ts +34 -0
- package/apps/sessrumnir/src/renderer/src/components/session-panel.tsx +711 -0
- package/apps/sessrumnir/src/renderer/src/components/session-runtime-indicator.tsx +43 -0
- package/apps/sessrumnir/src/renderer/src/components/settings-panel.tsx +1360 -0
- package/apps/sessrumnir/src/renderer/src/components/sidebar-activity.test.ts +47 -0
- package/apps/sessrumnir/src/renderer/src/components/sidebar-activity.ts +76 -0
- package/apps/sessrumnir/src/renderer/src/components/sidebar-session-labels.test.ts +73 -0
- package/apps/sessrumnir/src/renderer/src/components/sidebar-session-labels.ts +47 -0
- package/apps/sessrumnir/src/renderer/src/components/sidebar.tsx +1045 -0
- package/apps/sessrumnir/src/renderer/src/components/skills-panel.tsx +159 -0
- package/apps/sessrumnir/src/renderer/src/components/stats-panel.tsx +368 -0
- package/apps/sessrumnir/src/renderer/src/components/status-bar.tsx +274 -0
- package/apps/sessrumnir/src/renderer/src/components/status-popover.tsx +481 -0
- package/apps/sessrumnir/src/renderer/src/components/streaming-bubble.tsx +128 -0
- package/apps/sessrumnir/src/renderer/src/components/subagent-progress.tsx +254 -0
- package/apps/sessrumnir/src/renderer/src/components/task-launcher.tsx +168 -0
- package/apps/sessrumnir/src/renderer/src/components/terminal.tsx +203 -0
- package/apps/sessrumnir/src/renderer/src/components/theme-editor-helpers.test.ts +35 -0
- package/apps/sessrumnir/src/renderer/src/components/theme-editor-helpers.ts +35 -0
- package/apps/sessrumnir/src/renderer/src/components/theme-editor.tsx +440 -0
- package/apps/sessrumnir/src/renderer/src/components/theme-gallery.tsx +373 -0
- package/apps/sessrumnir/src/renderer/src/components/thinking-level-selector.tsx +85 -0
- package/apps/sessrumnir/src/renderer/src/components/timeline.tsx +289 -0
- package/apps/sessrumnir/src/renderer/src/components/tool-call-icon.ts +32 -0
- package/apps/sessrumnir/src/renderer/src/components/workflow-navigator.tsx +901 -0
- package/apps/sessrumnir/src/renderer/src/components/workspace-tabs.tsx +241 -0
- package/apps/sessrumnir/src/renderer/src/global.d.ts +8 -0
- package/apps/sessrumnir/src/renderer/src/hooks/use-folder-drop.ts +101 -0
- package/apps/sessrumnir/src/renderer/src/hooks.test.ts +88 -0
- package/apps/sessrumnir/src/renderer/src/hooks.ts +574 -0
- package/apps/sessrumnir/src/renderer/src/i18n.test.ts +54 -0
- package/apps/sessrumnir/src/renderer/src/i18n.ts +89 -0
- package/apps/sessrumnir/src/renderer/src/index.css +425 -0
- package/apps/sessrumnir/src/renderer/src/main.tsx +42 -0
- package/apps/sessrumnir/src/renderer/src/message-grouping.test.ts +422 -0
- package/apps/sessrumnir/src/renderer/src/message-grouping.ts +410 -0
- package/apps/sessrumnir/src/renderer/src/message-parsing.test.ts +86 -0
- package/apps/sessrumnir/src/renderer/src/message-parsing.ts +166 -0
- package/apps/sessrumnir/src/renderer/src/store-external-prompts.test.ts +161 -0
- package/apps/sessrumnir/src/renderer/src/store-session-transitions.test.ts +2090 -0
- package/apps/sessrumnir/src/renderer/src/store-timeline.test.ts +52 -0
- package/apps/sessrumnir/src/renderer/src/store-turn-error.test.ts +136 -0
- package/apps/sessrumnir/src/renderer/src/store.ts +3726 -0
- package/apps/sessrumnir/src/renderer/src/subagent-detection.test.ts +55 -0
- package/apps/sessrumnir/src/renderer/src/theme/engine.test.ts +29 -0
- package/apps/sessrumnir/src/renderer/src/theme/engine.ts +20 -0
- package/apps/sessrumnir/src/renderer/src/themes/breeze-claudius.json +69 -0
- package/apps/sessrumnir/src/renderer/src/themes/breeze-dark.json +69 -0
- package/apps/sessrumnir/src/renderer/src/themes/breeze-light.json +69 -0
- package/apps/sessrumnir/src/renderer/src/themes/builtin-themes.test.ts +95 -0
- package/apps/sessrumnir/src/renderer/src/themes/dark.json +46 -0
- package/apps/sessrumnir/src/renderer/src/themes/fensalir.json +82 -0
- package/apps/sessrumnir/src/renderer/src/themes/gruvbox.json +69 -0
- package/apps/sessrumnir/src/renderer/src/themes/index.ts +60 -0
- package/apps/sessrumnir/src/renderer/src/themes/light.json +46 -0
- package/apps/sessrumnir/src/renderer/src/themes/nord.json +69 -0
- package/apps/sessrumnir/src/renderer/src/themes/sessrumnir.json +82 -0
- package/apps/sessrumnir/src/renderer/src/utils/debounced-buffer.test.ts +81 -0
- package/apps/sessrumnir/src/renderer/src/utils/debounced-buffer.ts +57 -0
- package/apps/sessrumnir/src/renderer/src/utils/format-relative-time.test.ts +62 -0
- package/apps/sessrumnir/src/renderer/src/utils/format-relative-time.ts +28 -0
- package/apps/sessrumnir/src/renderer/src/utils/heatmap-grid.test.ts +57 -0
- package/apps/sessrumnir/src/renderer/src/utils/heatmap-grid.ts +41 -0
- package/apps/sessrumnir/src/renderer/src/utils/ipc-error.test.ts +22 -0
- package/apps/sessrumnir/src/renderer/src/utils/ipc-error.ts +12 -0
- package/apps/sessrumnir/src/renderer/src/utils/model-search.test.ts +63 -0
- package/apps/sessrumnir/src/renderer/src/utils/model-search.ts +20 -0
- package/apps/sessrumnir/src/renderer/src/utils/planning-prompt.test.ts +31 -0
- package/apps/sessrumnir/src/renderer/src/utils/planning-prompt.ts +27 -0
- package/apps/sessrumnir/src/renderer/src/utils/preview-load-error.test.ts +15 -0
- package/apps/sessrumnir/src/renderer/src/utils/preview-load-error.ts +22 -0
- package/apps/sessrumnir/src/renderer/src/utils/process-status-label.test.ts +13 -0
- package/apps/sessrumnir/src/renderer/src/utils/process-status-label.ts +19 -0
- package/apps/sessrumnir/src/renderer/src/utils/quick-switcher.test.ts +68 -0
- package/apps/sessrumnir/src/renderer/src/utils/quick-switcher.ts +52 -0
- package/apps/sessrumnir/src/renderer/src/utils/rank-file-results.test.ts +46 -0
- package/apps/sessrumnir/src/renderer/src/utils/rank-file-results.ts +23 -0
- package/apps/sessrumnir/src/renderer/src/utils/relative-time.tsx +25 -0
- package/apps/sessrumnir/src/renderer/src/utils/session-title.test.ts +98 -0
- package/apps/sessrumnir/src/renderer/src/utils/session-title.ts +58 -0
- package/apps/sessrumnir/src/renderer/src/utils/stale-guard.test.ts +29 -0
- package/apps/sessrumnir/src/renderer/src/utils/stale-guard.ts +20 -0
- package/apps/sessrumnir/src/renderer/src/utils/theme.test.ts +174 -0
- package/apps/sessrumnir/src/renderer/src/utils/theme.ts +159 -0
- package/apps/sessrumnir/src/renderer/src/utils/workflow-runs.test.ts +130 -0
- package/apps/sessrumnir/src/renderer/src/utils/workflow-runs.ts +84 -0
- package/apps/sessrumnir/src/renderer/tsconfig.json +24 -0
- package/apps/sessrumnir/src/shared/agent-engine-label.test.ts +45 -0
- package/apps/sessrumnir/src/shared/agent-engine-label.ts +53 -0
- package/apps/sessrumnir/src/shared/app-settings.test.ts +25 -0
- package/apps/sessrumnir/src/shared/app-settings.ts +23 -0
- package/apps/sessrumnir/src/shared/bridge-http.ts +341 -0
- package/apps/sessrumnir/src/shared/bridge.ts +337 -0
- package/apps/sessrumnir/src/shared/council-config.test.ts +300 -0
- package/apps/sessrumnir/src/shared/council-config.ts +356 -0
- package/apps/sessrumnir/src/shared/default-settings.ts +39 -0
- package/apps/sessrumnir/src/shared/folder-drop.test.ts +118 -0
- package/apps/sessrumnir/src/shared/folder-drop.ts +74 -0
- package/apps/sessrumnir/src/shared/fork-point.test.ts +30 -0
- package/apps/sessrumnir/src/shared/fork-point.ts +24 -0
- package/apps/sessrumnir/src/shared/i18n/i18next.d.ts +10 -0
- package/apps/sessrumnir/src/shared/i18n/index.test.ts +51 -0
- package/apps/sessrumnir/src/shared/i18n/index.ts +50 -0
- package/apps/sessrumnir/src/shared/i18n/languages.ts +12 -0
- package/apps/sessrumnir/src/shared/i18n/locale-checks.ts +98 -0
- package/apps/sessrumnir/src/shared/i18n/locales.test.ts +92 -0
- package/apps/sessrumnir/src/shared/i18n/pseudo.test.ts +39 -0
- package/apps/sessrumnir/src/shared/i18n/pseudo.ts +44 -0
- package/apps/sessrumnir/src/shared/i18n/resolve.test.ts +43 -0
- package/apps/sessrumnir/src/shared/i18n/resolve.ts +41 -0
- package/apps/sessrumnir/src/shared/i18n/resources.ts +11 -0
- package/apps/sessrumnir/src/shared/ipc-contracts.ts +1539 -0
- package/apps/sessrumnir/src/shared/models-config.test.ts +124 -0
- package/apps/sessrumnir/src/shared/models-config.ts +167 -0
- package/apps/sessrumnir/src/shared/package-filter.ts +19 -0
- package/apps/sessrumnir/src/shared/package-spec.test.ts +51 -0
- package/apps/sessrumnir/src/shared/package-spec.ts +25 -0
- package/apps/sessrumnir/src/shared/path-compare.test.ts +38 -0
- package/apps/sessrumnir/src/shared/path-compare.ts +76 -0
- package/apps/sessrumnir/src/shared/permission-mode.ts +8 -0
- package/apps/sessrumnir/src/shared/pi-command.test.ts +165 -0
- package/apps/sessrumnir/src/shared/pi-command.ts +139 -0
- package/apps/sessrumnir/src/shared/product-name.ts +7 -0
- package/apps/sessrumnir/src/shared/session-lineage.test.ts +103 -0
- package/apps/sessrumnir/src/shared/session-lineage.ts +50 -0
- package/apps/sessrumnir/src/shared/session-preview.test.ts +93 -0
- package/apps/sessrumnir/src/shared/session-preview.ts +56 -0
- package/apps/sessrumnir/src/shared/sidebar-width.test.ts +58 -0
- package/apps/sessrumnir/src/shared/sidebar-width.ts +37 -0
- package/apps/sessrumnir/src/shared/theme/builtin-ids.ts +16 -0
- package/apps/sessrumnir/src/shared/theme/resolve.test.ts +66 -0
- package/apps/sessrumnir/src/shared/theme/resolve.ts +29 -0
- package/apps/sessrumnir/src/shared/theme/syntax-defaults.ts +24 -0
- package/apps/sessrumnir/src/shared/theme/theme-file.test.ts +88 -0
- package/apps/sessrumnir/src/shared/theme/theme-file.ts +132 -0
- package/apps/sessrumnir/src/shared/theme/tokens.test.ts +35 -0
- package/apps/sessrumnir/src/shared/theme/tokens.ts +86 -0
- package/apps/sessrumnir/src/shared/tsconfig.json +17 -0
- package/apps/sessrumnir/src/shared/untrusted-data.test.ts +32 -0
- package/apps/sessrumnir/src/shared/untrusted-data.ts +22 -0
- package/apps/sessrumnir/src/shared/version-compare.test.ts +33 -0
- package/apps/sessrumnir/src/shared/version-compare.ts +50 -0
- package/apps/sessrumnir/src/shared/workflow-control.ts +33 -0
- package/apps/sessrumnir/src/web-server.ts +315 -0
- package/apps/sessrumnir/tsconfig.json +26 -0
- package/apps/smidja/smidja_build.py +45 -0
- package/apps/smidja/smidja_build_review.py +76 -0
- package/apps/smidja/smidja_build_test.py +79 -0
- package/apps/smidja/smidja_document.py +76 -0
- package/apps/smidja/smidja_modules/agent_cc.py +15 -0
- package/apps/smidja/smidja_modules/agent_opencode.py +194 -0
- package/apps/smidja/smidja_modules/agent_pi.py +392 -0
- package/apps/smidja/smidja_modules/agents.py +993 -0
- package/apps/smidja/smidja_modules/changes.py +103 -0
- package/apps/smidja/smidja_modules/console.py +131 -0
- package/apps/smidja/smidja_modules/data_types.py +447 -0
- package/apps/smidja/smidja_modules/gates.py +108 -0
- package/apps/smidja/smidja_modules/git_helper.py +120 -0
- package/apps/smidja/smidja_modules/lm_local.py +144 -0
- package/apps/smidja/smidja_modules/permissions.py +194 -0
- package/apps/smidja/smidja_modules/prompts.py +21 -0
- package/apps/smidja/smidja_modules/quality.py +240 -0
- package/apps/smidja/smidja_modules/runner.py +155 -0
- package/apps/smidja/smidja_modules/session.py +50 -0
- package/apps/smidja/smidja_modules/stall_watch.py +149 -0
- package/apps/smidja/smidja_modules/tracer.py +271 -0
- package/apps/smidja/smidja_modules/utils.py +77 -0
- package/apps/smidja/smidja_orchestrate.py +162 -0
- package/apps/smidja/smidja_plan.py +45 -0
- package/apps/smidja/smidja_plan_build.py +55 -0
- package/apps/smidja/smidja_plan_build_test.py +86 -0
- package/apps/smidja/smidja_plan_build_test_quality.py +98 -0
- package/apps/smidja/smidja_prompt.py +44 -0
- package/apps/smidja/smidja_quality.py +49 -0
- package/apps/smidja/smidja_recon_iv.py +202 -0
- package/apps/smidja/smidja_scout.py +45 -0
- package/apps/smidja/smidja_simple_sdlc.py +183 -0
- package/apps/smidja/smidja_smidja_config/smidja.config.yaml +143 -0
- package/apps/smidja-factory/AGENTS.md +165 -0
- package/apps/smidja-factory/README.md +3 -0
- package/apps/smidja-factory/SKILL.md +18 -0
- package/apps/smidja-factory/apps/visualizer/.oxlintrc.json +18 -0
- package/apps/smidja-factory/apps/visualizer/bun.lock +269 -0
- package/apps/smidja-factory/apps/visualizer/desktop/icon.png +0 -0
- package/apps/smidja-factory/apps/visualizer/desktop/main.js +140 -0
- package/apps/smidja-factory/apps/visualizer/desktop/package-lock.json +871 -0
- package/apps/smidja-factory/apps/visualizer/desktop/package.json +18 -0
- package/apps/smidja-factory/apps/visualizer/desktop/preload.js +19 -0
- package/apps/smidja-factory/apps/visualizer/index.html +28 -0
- package/apps/smidja-factory/apps/visualizer/package.json +31 -0
- package/apps/smidja-factory/apps/visualizer/public/icon.svg +7 -0
- package/apps/smidja-factory/apps/visualizer/public/models/claude.png +0 -0
- package/apps/smidja-factory/apps/visualizer/public/models/gemini.png +0 -0
- package/apps/smidja-factory/apps/visualizer/public/models/kimi.png +0 -0
- package/apps/smidja-factory/apps/visualizer/public/models/openai.png +0 -0
- package/apps/smidja-factory/apps/visualizer/public/models/zai.png +0 -0
- package/apps/smidja-factory/apps/visualizer/server/chat.ts +425 -0
- package/apps/smidja-factory/apps/visualizer/server/db.ts +1050 -0
- package/apps/smidja-factory/apps/visualizer/server/index.ts +648 -0
- package/apps/smidja-factory/apps/visualizer/server/model-catalog.ts +90 -0
- package/apps/smidja-factory/apps/visualizer/server/roster-api.ts +75 -0
- package/apps/smidja-factory/apps/visualizer/server/roster_resolve.py +170 -0
- package/apps/smidja-factory/apps/visualizer/server/settings.ts +106 -0
- package/apps/smidja-factory/apps/visualizer/shared/types.ts +630 -0
- package/apps/smidja-factory/apps/visualizer/src/App.vue +379 -0
- package/apps/smidja-factory/apps/visualizer/src/components/ChatInput.vue +155 -0
- package/apps/smidja-factory/apps/visualizer/src/components/ChatThread.vue +219 -0
- package/apps/smidja-factory/apps/visualizer/src/components/ChatView.vue +335 -0
- package/apps/smidja-factory/apps/visualizer/src/components/DecisionsView.vue +177 -0
- package/apps/smidja-factory/apps/visualizer/src/components/DetailSection.vue +76 -0
- package/apps/smidja-factory/apps/visualizer/src/components/MemoryView.vue +397 -0
- package/apps/smidja-factory/apps/visualizer/src/components/PhaseDetail.vue +1354 -0
- package/apps/smidja-factory/apps/visualizer/src/components/PhaseDots.vue +55 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SessionCard.vue +583 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SessionMiniCard.vue +58 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SessionPanel.vue +272 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SessionStartForm.vue +152 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SessionTrace.vue +1157 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SessionsList.vue +96 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SettingsView.vue +152 -0
- package/apps/smidja-factory/apps/visualizer/src/components/StatChip.vue +98 -0
- package/apps/smidja-factory/apps/visualizer/src/components/StatsView.vue +382 -0
- package/apps/smidja-factory/apps/visualizer/src/components/StatusChip.vue +73 -0
- package/apps/smidja-factory/apps/visualizer/src/components/ToolCallCard.vue +156 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/api.ts +177 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/chat-api.ts +95 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/chat-store.ts +351 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/events.ts +131 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/format.ts +92 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/highlight.ts +54 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/markdown.ts +120 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/models.ts +42 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/router.ts +67 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/types.ts +61 -0
- package/apps/smidja-factory/apps/visualizer/src/main.ts +13 -0
- package/apps/smidja-factory/apps/visualizer/src/style.css +592 -0
- package/apps/smidja-factory/apps/visualizer/tsconfig.json +30 -0
- package/apps/smidja-factory/apps/visualizer/vite.config.js +32 -0
- package/apps/smidja-factory/apps/visualizer/vite.config.ts +31 -0
- package/apps/smidja-factory/cookbooks/create_config.md +61 -0
- package/apps/smidja-factory/cookbooks/create_smidja.md +120 -0
- package/apps/smidja-factory/cookbooks/how_to_prompt_for_the_eng.md +110 -0
- package/apps/smidja-factory/cookbooks/install.md +56 -0
- package/apps/smidja-factory/cookbooks/run_smidja.md +122 -0
- package/apps/smidja-factory/cookbooks/smidja_overview.md +88 -0
- package/apps/smidja-factory/cookbooks/update_config.md +103 -0
- package/apps/smidja-factory/cookbooks/update_modules.md +100 -0
- package/apps/smidja-factory/cookbooks/update_smidja.md +90 -0
- package/apps/smidja-factory/docs/enhancement-plan.md +420 -0
- package/apps/smidja-factory/references/config.md +202 -0
- package/apps/smidja-factory/references/handoff.md +161 -0
- package/apps/smidja-factory/references/observability.md +166 -0
- package/apps/smidja-factory/scripts/install.py +113 -0
- package/apps/smidja-factory/scripts/make_config.py +35 -0
- package/apps/smidja-factory/scripts/make_smidja.py +119 -0
- package/apps/smidja-factory/skills/add-or-edit-ai-models/SKILL.md +151 -0
- package/apps/smidja-factory/skills/create-new-agent/SKILL.md +125 -0
- package/apps/smidja-factory/skills/create-new-teams/SKILL.md +151 -0
- package/apps/smidja-factory/skills/git-ops/README.md +25 -0
- package/apps/smidja-factory/skills/git-ops/SKILL.md +18 -0
- package/apps/smidja-factory/skills/git-ops/create_branch.sh +79 -0
- package/apps/smidja-factory/skills/git-ops/safe_commit.sh +82 -0
- package/apps/smidja-factory/skills/git-ops/sync_upstream.sh +94 -0
- package/apps/smidja-factory/skills/how-to-run-agent-teams/SKILL.md +117 -0
- package/apps/smidja-factory/skills/smidja-instructions/SKILL.md +168 -0
- package/apps/smidja-factory/skills/smidja-launcher/SKILL.md +155 -0
- package/apps/smidja-factory/skills/smidja-start/SKILL.md +266 -0
- package/apps/smidja-factory/skills/volundr/SKILL.md +232 -0
- package/apps/smidja-factory/skills/volundr/prompt/dispatch-prompt.md +91 -0
- package/apps/smidja-factory/skills/volundr/prompt/volundr-system.md +63 -0
- package/apps/smidja-factory/skills/volundr/scripts/volundr-observe.py +52 -0
- package/apps/smidja-factory/skills/volundr/scripts/volundr-recall.py +54 -0
- package/apps/smidja-factory/skills/volundr/scripts/volundr-status.py +51 -0
- package/apps/smidja-factory/skills/volundr/scripts/volundr-teach.py +47 -0
- package/apps/smidja-factory/templates/env.sample +28 -0
- package/apps/smidja-factory/templates/harness_engineering/subagents.ts +706 -0
- package/apps/smidja-factory/templates/harness_engineering/themeMap.ts +145 -0
- package/apps/smidja-factory/templates/justfile +89 -0
- package/apps/smidja-factory/templates/prompt_engineering/builder/system.md +13 -0
- package/apps/smidja-factory/templates/prompt_engineering/builder/user.md +34 -0
- package/apps/smidja-factory/templates/prompt_engineering/documenter/system.md +17 -0
- package/apps/smidja-factory/templates/prompt_engineering/documenter/user.md +48 -0
- package/apps/smidja-factory/templates/prompt_engineering/orchestrator/system.md +55 -0
- package/apps/smidja-factory/templates/prompt_engineering/orchestrator/user.md +39 -0
- package/apps/smidja-factory/templates/prompt_engineering/planner/system.md +21 -0
- package/apps/smidja-factory/templates/prompt_engineering/planner/user.md +45 -0
- package/apps/smidja-factory/templates/prompt_engineering/recon_orchestrator/system.md +51 -0
- package/apps/smidja-factory/templates/prompt_engineering/recon_orchestrator/user.md +26 -0
- package/apps/smidja-factory/templates/prompt_engineering/reviewer/system.md +16 -0
- package/apps/smidja-factory/templates/prompt_engineering/reviewer/user.md +44 -0
- package/apps/smidja-factory/templates/prompt_engineering/scout/system.md +32 -0
- package/apps/smidja-factory/templates/prompt_engineering/scout/user.md +34 -0
- package/apps/smidja-factory/templates/smidja/smidja_build.py +45 -0
- package/apps/smidja-factory/templates/smidja/smidja_build_review.py +76 -0
- package/apps/smidja-factory/templates/smidja/smidja_build_test.py +79 -0
- package/apps/smidja-factory/templates/smidja/smidja_document.py +76 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/agent_cc.py +15 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/agent_opencode.py +194 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/agent_pi.py +392 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/agents.py +993 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/changes.py +103 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/console.py +131 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/data_types.py +447 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/gates.py +108 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/git_helper.py +120 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/lm_local.py +144 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/permissions.py +194 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/prompts.py +21 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/quality.py +240 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/runner.py +155 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/session.py +50 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/stall_watch.py +149 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/tracer.py +271 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/utils.py +77 -0
- package/apps/smidja-factory/templates/smidja/smidja_orchestrate.py +162 -0
- package/apps/smidja-factory/templates/smidja/smidja_plan.py +45 -0
- package/apps/smidja-factory/templates/smidja/smidja_plan_build.py +55 -0
- package/apps/smidja-factory/templates/smidja/smidja_plan_build_test.py +86 -0
- package/apps/smidja-factory/templates/smidja/smidja_plan_build_test_quality.py +98 -0
- package/apps/smidja-factory/templates/smidja/smidja_prompt.py +44 -0
- package/apps/smidja-factory/templates/smidja/smidja_quality.py +49 -0
- package/apps/smidja-factory/templates/smidja/smidja_recon_iv.py +202 -0
- package/apps/smidja-factory/templates/smidja/smidja_scout.py +45 -0
- package/apps/smidja-factory/templates/smidja/smidja_simple_sdlc.py +183 -0
- package/apps/smidja-factory/templates/smidja.config.yaml +143 -0
- package/bin/app-build.sh +36 -0
- package/bin/electron-lib.sh +25 -0
- package/bin/sessrumnir.sh +1 -1
- package/bin/ymir-install.sh +7 -0
- package/install.sh +88 -0
- package/package.json +16 -7
- package/scripts/electron.sh +6 -2
- package/.agents/bus/messages.json +0 -1
- package/.agents/config/agents.machine.example.yaml +0 -16
- package/.pi/mcp.json +0 -16
- package/assets/reference/state/.branch-eligible-owner +0 -4
- package/assets/reference/state/.branch-mirror-cursor +0 -1
- package/assets/reference/state/.branch-outcomes-cursor +0 -1
- package/assets/reference/state/.branch-session +0 -1
- package/assets/reference/state/.count-default_wF_p5 +0 -1
- package/assets/reference/state/.count-default_wF_p6 +0 -1
- package/assets/reference/state/.count-default_wR_p2 +0 -1
- package/assets/reference/state/.count-default_wS_p2 +0 -1
- package/assets/reference/state/.count-firstmate_fm-server-knowledge +0 -1
- package/assets/reference/state/.hash-default_wF_p5 +0 -1
- package/assets/reference/state/.hash-default_wF_p6 +0 -1
- package/assets/reference/state/.hash-default_wR_p2 +0 -1
- package/assets/reference/state/.hash-default_wS_p2 +0 -1
- package/assets/reference/state/.hash-firstmate_fm-server-knowledge +0 -1
- package/assets/reference/state/.heartbeat-streak +0 -1
- package/assets/reference/state/.inactive-outcome-reconcile +0 -2
- package/assets/reference/state/.lock +0 -1
- package/assets/reference/state/.pi-branch-extension-loaded +0 -1
- package/assets/reference/state/.pi-turnend-extension-loaded +0 -2
- package/assets/reference/state/.pi-watch-extension-loaded +0 -2
- package/assets/reference/state/.seen-server-knowledge_turn-ended +0 -1
- package/assets/reference/state/.session-start-agents-baseline +0 -2
- package/assets/reference/state/.session-start-complete +0 -1
- package/assets/reference/state/.stale-default_wF_p5 +0 -1
- package/assets/reference/state/.stale-default_wR_p2 +0 -1
- package/assets/reference/state/.stale-firstmate_fm-server-knowledge +0 -1
- package/assets/reference/state/.stale-since-default_wF_p5 +0 -1
- package/assets/reference/state/.stale-since-default_wR_p2 +0 -1
- package/assets/reference/state/.stale-since-firstmate_fm-server-knowledge +0 -1
- package/assets/reference/state/.startup-network.delivered +0 -1
- package/assets/reference/state/.startup-network.report +0 -1
- package/assets/reference/state/.startup-network.status +0 -10
- package/assets/reference/state/.startup-network.timings +0 -6
- package/assets/reference/state/.trace-context-effective +0 -1
- package/assets/reference/state/.wake-queue +0 -0
- package/assets/reference/state/.wake-queue.seq +0 -1
- package/assets/reference/state/.wake-rows.consume.BcqXSq +0 -0
- package/assets/reference/state/.wake-rows.consume.NDZZei +0 -0
- package/assets/reference/state/.wake-rows.consume.VGkhYm +0 -0
- package/assets/reference/state/.wake-rows.consume.tR2rTy +0 -0
- package/assets/reference/state/.watch-arm-output.3581p6 +0 -0
- package/assets/reference/state/.watch-arm-output.3ciDNP +0 -0
- package/assets/reference/state/.watch-arm-output.SksR6k +0 -0
- package/assets/reference/state/.watch-cycle-exits.log +0 -22
- package/assets/reference/state/.watch-deliveries.log +0 -19
- package/assets/reference/state/.watch-triage.log +0 -38
- package/assets/reference/state/.watch.lock/fm-home +0 -1
- package/assets/reference/state/.watch.lock/pid +0 -1
- package/assets/reference/state/.watch.lock/pid-identity +0 -1
- package/assets/reference/state/.watch.lock/watcher-path +0 -1
- package/assets/reference/state/.watch.lock.owner.qqbxmS/fm-home +0 -1
- package/assets/reference/state/.watch.lock.owner.qqbxmS/pid +0 -1
- package/assets/reference/state/.watch.lock.owner.qqbxmS/pid-identity +0 -1
- package/assets/reference/state/.watch.lock.owner.qqbxmS/watcher-path +0 -1
- package/assets/reference/state/.watcher-down +0 -1
- package/assets/reference/state/branch-outcomes.jsonl +0 -1
- package/assets/reference/state/branch-session/2026-09-04T16-49-38-671Z_01a06d53-6b6f-7619-930d-e6dfd9a4a29e.jsonl +0 -211
- package/assets/reference/state/home-summary.json +0 -42
- package/assets/reference/state/server-knowledge.busy-gen +0 -1
- package/assets/reference/state/server-knowledge.busy-state +0 -1
- package/assets/reference/state/server-knowledge.herdr-presentation +0 -12
- package/assets/reference/state/server-knowledge.meta +0 -18
- package/assets/reference/state/server-knowledge.pi-ext.ts +0 -26
- package/assets/reference/state/server-knowledge.turn-ended +0 -0
- /package/{assets/reference/state/.last-check → apps/hlidskjalf/src/app/.gitkeep} +0 -0
- /package/{assets/reference/state/.last-heartbeat → apps/hlidskjalf/src/components/.gitkeep} +0 -0
- /package/{assets/reference/state/.last-watcher-beat → apps/hlidskjalf/src/services/.gitkeep} +0 -0
- /package/{assets/reference/state/.main-eligible-rows.tmp.p2HMKo → apps/smidja/smidja_modules/__init__.py} +0 -0
- /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
|
+
}
|